diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000000..47de40687da --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": [ + "es2015", + "stage-2" + ], + "plugins": [ + "add-module-exports" + ] +} diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000000..a11de13a5c8 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,37 @@ +engines: + phpcodesniffer: + enabled: true + config: + standard: "WordPress" + eslint: + enabled: true + scss-lint: + enabled: true + duplication: + enabled: true + config: + languages: + - php + - javascript +ratings: + paths: + - "includes/*" +exclude_paths: + - tests/* + - apigen/* + - dummy-data/* + - i18n/* + - includes/api/legacy/* + - includes/libraries/* + - includes/updates/* + - includes/gateways/simplify-commerce/* + - includes/shipping/legacy-* + - includes/wc-deprecated-functions.php + - includes/class-wc-legacy-api.php + - assets/js/accounting/** + - assets/js/jquery-* + - assets/js/prettyPhoto/* + - assets/js/round/* + - assets/js/select2/* + - assets/js/stupidtable/* + - assets/js/zeroclipboard/* diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000000..c00a1ca9322 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +src_dir: . +coverage_clover: ./tmp/clover.xml +json_path: ./tmp/coveralls-upload.json diff --git a/.editorconfig b/.editorconfig index e66ad499f4f..c3dfa83750f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,9 @@ +# This file is for unifying the coding style for different editors and IDEs # editorconfig.org + +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + root = true [*] @@ -10,3 +15,10 @@ indent_style = tab insert_final_newline = true trim_trailing_whitespace = true +[*.txt] +trim_trailing_whitespace = false + +[*.{md,json,yml}] +trim_trailing_whitespace = false +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..5e8686b5f3a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +/.* export-ignore +apigen* export-ignore +CHANGELOG.txt export-ignore +composer.* export-ignore +Gruntfile.js export-ignore +package.json export-ignore +phpcs.ruleset.xml export-ignore +phpunit.* export-ignore +README.md export-ignore +tests export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000000..590be3a9bf2 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# How To Contribute + +Community made patches, localizations, bug reports and contributions are always welcome and crucial to ensure WooCommerce remains the leading eCommerce platform for WordPress. WooCommerce currently powers 30% of all online stores across the internet, and your help making it even more awesome will be greatly appreciated :) + +When contributing please ensure you follow the guidelines below to help us keep on top of things. + +__Please Note:__ + +GitHub is for _bug reports and contributions only_ - if you have a support question or a request for a customization this is not the right place to post it. Use [WooCommerce Support](https://support.woocommerce.com) for customer support, [WordPress.org](https://wordpress.org/support/plugin/woocommerce) for community support, and for customizations we recommend one of the following services: + +- [WooExperts](https://woocommerce.com/experts/) +- [Codeable](https://codeable.io/) + +## Contributing to Core + +### Reporting Issues + +Reporting issues is a great way to became a contributor as it doesn't require technical skills. In fact you don't even need to know a programming language or to be able to check the code itself, you just need to make sure that everything works as expected and [submit an issue report](https://github.com/woocommerce/woocommerce/issues/new) if you spot a bug. Sound like something you're up for? Go for it! + +#### How To Submit An Issue Report + +If something isn't working, congratulations you've found a bug! Help us fix it by submitting an issue report: + +* Make sure you have a [GitHub account](https://github.com/signup/free) +* Search the [Existing Issues](https://github.com/woocommerce/woocommerce/issues) to be sure that the one you've noticed isn't already there +* Submit a report for your issue + * Clearly describe the issue (including steps to reproduce it if it's a bug) + * Make sure you fill in the earliest version that you know has the issue. + +### Making Changes + +Making changes to the core is a key way to help us improve WooCommerce. You will need some technical skills to make a change, like knowing a bit of PHP, CSS, SASS or JavaScript. + +If you think something could be improved and you're able to do so, make your changes and submit a Pull Request. We'll be pleased to get it :) + +#### How To Submit A PR + +* Fork the repository on GitHub +* Make the changes to your forked repository + * **Ensure you stick to the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/coding-standards/php/).** + * Ensure you use LF line endings - no crazy Windows line endings please :) +* When committing, reference your issue number (#1234) and include a note about the fix +* Push the changes to your fork and submit a pull request on the master branch of the WooCommerce repository. Existing maintenance branches will be maintained by WooCommerce developers +* Please **don't** modify the changelog - this will be maintained by the WooCommerce developers. +* Please **don't** add your localizations or update the .pot files - these will also be maintained by the WooCommerce developers. To contribute to the localization of WooCommerce, please join the [translate.wordpress.org project](https://translate.wordpress.org/projects/wp-plugins/woocommerce). This is much needed, if you speak a language that needs translating consider yourself officially invited to the party. + +After you follow the step above, the next stage will be waiting on us to merge your Pull Request. We review them all, and make suggestions and changes as and if necessary. + +## Contribute To Localizing WooCommerce + +Localization is a very important part of WooCommerce. We believe in net neutrality and want our platform to be available to everyone, everywhere with equal ease. When you localize WooCommerce, you are helping hundreds of people in the world, and all the people who speak your language. That's pretty neat. + +### Glossary & Style Guide + +Please refer to this page on the [Translator Handbook](https://make.wordpress.org/polyglots/handbook/translating/glossary-style-guide/) for information about the glossary and the style guide. + +We maintain the WooCommerce glossary [on this shared Google Sheet](https://docs.google.com/spreadsheets/d/1Pobl2nNWieaSpZND9-Bwa4G8pnMU7QYceKsXuWCwSxQ/edit?usp=sharing). You can use it as a template for creating your own glossary. +Please download the file by going to **File > Download as > Comma-separated values (.csv, current sheet)** and save it on your computer/Mac. Open it with your favourite CSV editor (or re-upload it on your own Google Drive) and edit it. + +Make sure to edit the second column’s header by using your own language’s code (eg. for Italian you would use `it`, for Portuguese (Brazil) you would use `pt-BR`). + +Write the translated entry in this column and translate the entry description as well. +Don’t change other columns headers and value, but feel free to add new entries. + +When your CSV is ready, import it on GlotPress. + +_**Warning**: Importing a CSV does not replace existing items, they will be created again. We suggest to import them only when first creating the glossary._ + +Each translation editor will take care of updating the glossary on GlotPress by editing/adding items when needed. + +_**Note**: Only editors can create/import and edit glossaries and glossary items on GlotPress. Anyone can suggest new items to add to the glossary or translate them._ + +**Style Guides Available** + +We don’t have a Style Guide template available, so feel free to create your own. Here are the style guides available at the moment: + +* [Italian](https://docs.google.com/document/d/1rspopHOiTL-5-PjyG5eJxjkYk6JkzqVbyS24OdA052o/edit?usp=sharing) + +If you created a style guide for your language, please let us know so we can add it in the list above. You can also add it by yourself by submitting a PR for this file. + +### Translating Core + +We have a [project on translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/woocommerce). You can join the localization team of your language and help by translating WooCommerce. [Find more about using joining a language team and using GlotPress](https://make.wordpress.org/polyglots/handbook/tools/glotpress-translate-wordpress-org/). + +If WooCommerce is already 100% translated for your language, join the team anyway! We regularly update our language files and there will definitely be need of your help soon. + +### Translating Video Tutorials + +Another valuable way to help is by translating our growing library of WooCommerce video tutorials. Check out the [Translating Our Videos](https://docs.woocommerce.com/document/translating-our-videos/) doc and join in! + +By translating video tutorials you'll be helping non-English speaking users and people affected by disabilities to get to grips with using WooCommerce for the first time, and to go on and create their businesses and make a living! That's something to be proud of and if you choose to dive into this area, we salute you. + +# Additional Resources + +* [General GitHub documentation](https://help.github.com/) +* [GitHub pull request documentation](https://help.github.com/articles/about-pull-requests/) +* [Translator Handbook](https://make.wordpress.org/polyglots/handbook/) +* [WooCommerce Docs](https://docs.woocommerce.com/) +* [WooCommerce Support](https://support.woocommerce.com) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..82fafab64d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,35 @@ +## Prerequisites + + + +- [ ] 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 +- [ ] This is not a usage question (Those should be directed to the [community](https://wordpress.org/support/plugin/woocommerce), unless this is a question about a premium plugin in which you should [use the helpdesk](https://woocommerce.com/my-account/tickets/) for official extensions or contact the author of 3rd party extensions) +- [ ] 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 + +1. +2. +3. + +## Expected behavior and actual behavior + +When I follow those steps, I see... + +I was expecting... + +## Environment + +
+``` +Grab the system status report from WooCommerce > System Status and paste it here. +``` +
+ +## Isolating the problem + +- [ ] 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 diff --git a/.github/wiki.png b/.github/wiki.png new file mode 100644 index 00000000000..539760052fa Binary files /dev/null and b/.github/wiki.png differ diff --git a/.gitignore b/.gitignore index c29bd24148c..0612a52c0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,41 @@ -/nbproject/private/ -/node_modules/ +# Editors project.xml project.properties -.DS_Store -Thumbs.db +/nbproject/private/ .buildpath .project .settings* -sftp-config.json -/deploy/ +.idea + +# Grunt +/node_modules/ + +# Sass +.sass-cache/ + +# OS X metadata +.DS_Store + +# Windows junk +Thumbs.db + +# ApiGen /wc-apidocs/ -# Ignore all log files except for .htaccess -/logs/* -!/logs/.htaccess +# Behat/CLI Tests +tests/cli/installer +tests/cli/composer.phar +tests/cli/composer.lock +tests/cli/composer.json +tests/cli/vendor + +# Unit tests +/tmp +/tests/bin/tmp +/tests/e2e-tests/config/local-*.json + +# Logs +/logs + +# Composer +/vendor/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000000..7ed49c535c3 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,25 @@ +{ + "boss": true, + "curly": true, + "eqeqeq": true, + "eqnull": true, + "es3": true, + "expr": true, + "immed": true, + "noarg": true, + "onevar": true, + "quotmark": "single", + "trailing": true, + "undef": true, + "unused": true, + + "browser": true, + + "globals": { + "_": false, + "Backbone": false, + "jQuery": false, + "JSON": false, + "wp": false + } +} diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 00000000000..07af2c45c90 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,32 @@ +filter: + excluded_paths: + - tests/* + - apigen/* + - dummy-data/* + - i18n/* + - includes/api/legacy/* + - includes/legacy/* + - includes/libraries/* + - includes/updates/* + - includes/gateways/simplify-commerce/* + - includes/shipping/legacy-* + - includes/wc-deprecated-functions.php + - includes/class-wc-legacy-api.php + +checks: + php: + variable_existence: false + verify_access_scope_valid: false + verify_argument_usable_as_reference: false + verify_property_names: false + no_global_keyword: false + psr2_switch_declaration: false + psr2_control_structure_declaration: false + psr2_class_declaration: false + one_class_per_file: false + no_exit: false + avoid_superglobals: false + avoid_closing_tag: false + +tools: + sensiolabs_security_checker: true diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 00000000000..4c93dd4476d --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,60 @@ +{ + "rules": { + "indentation": "tab", + "color-hex-case": "lower", + "color-no-invalid-hex": true, + + "function-calc-no-unspaced-operator": true, + "function-comma-space-after": "always-single-line", + "function-comma-space-before": "never", + "function-name-case": "lower", + "function-url-quotes": "always", + "function-whitespace-after": "always", + + "number-leading-zero": "always", + "number-no-trailing-zeros": true, + "length-zero-no-unit": true, + + "string-no-newline": true, + "string-quotes": "single", + + "unit-case": "lower", + "unit-no-unknown": true, + "unit-whitelist": ["px", "%", "deg", "ms", "em", "vh", "vw", "rem", "s", "ex", "pt", "cm"], + + "value-list-comma-space-after": "always-single-line", + "value-list-comma-space-before": "never", + + "shorthand-property-no-redundant-values": true, + + "property-case": "lower", + + "declaration-block-no-duplicate-properties": [true, { "severity": "warning" } ], + "declaration-block-no-ignored-properties": [true, { "severity": "warning" } ], + "declaration-block-trailing-semicolon": "always", + "declaration-block-single-line-max-declarations": 0, + "declaration-block-semicolon-space-before": "never", + "declaration-block-semicolon-space-after": "always-single-line", + "declaration-block-semicolon-newline-before": "never-multi-line", + "declaration-block-semicolon-newline-after": "always-multi-line", + + "block-closing-brace-newline-after": "always", + "block-closing-brace-newline-before": "always-multi-line", + "block-no-empty": true, + "block-opening-brace-newline-after": "always-multi-line", + "block-opening-brace-space-before": "always", + + "selector-attribute-brackets-space-inside": "never", + "selector-attribute-operator-space-after": "never", + "selector-attribute-operator-space-before": "never", + "selector-combinator-space-after": "always", + "selector-combinator-space-before": "always", + "selector-pseudo-class-case": "lower", + "selector-pseudo-class-parentheses-space-inside": "always", + "selector-pseudo-element-case": "lower", + "selector-pseudo-element-colon-notation": "double", + "selector-pseudo-element-no-unknown": true, + "selector-type-case": "lower", + "selector-no-id": [true, { "severity": "warning" } ], + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..81349fccdbe --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: php + +sudo: false + +# Test main supported versions of PHP and HHVM against latest WP. 5.2 is min supported version. +php: + - 5.2 + - 5.3 + - 5.6 + - 7.0 + - 7.1 + +env: + - WP_VERSION=latest WP_MULTISITE=0 PHP_LATEST_STABLE=7.1 + +# Additonal tests against stable PHP (min recommended version is 5.6) and past supported versions of WP. +matrix: + include: + - php: 5.6 + env: WP_VERSION=latest WP_MULTISITE=1 PHP_LATEST_STABLE=7.1 + +before_script: + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - bash tests/bin/install.sh woocommerce_test root '' localhost $WP_VERSION + - bash tests/bin/travis.sh before + +script: + - bash tests/bin/phpunit.sh + - bash tests/bin/travis.sh during + +after_script: + - bash tests/bin/travis.sh after diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png new file mode 100644 index 00000000000..d515d551fcc Binary files /dev/null and b/.wordpress-org/banner-1544x500.png differ diff --git a/.wordpress-org/banner-772x250.png b/.wordpress-org/banner-772x250.png new file mode 100644 index 00000000000..aa1f423b1bd Binary files /dev/null and b/.wordpress-org/banner-772x250.png differ diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png new file mode 100644 index 00000000000..60918af4695 Binary files /dev/null and b/.wordpress-org/icon-128x128.png differ diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png new file mode 100644 index 00000000000..2390aa84fff Binary files /dev/null and b/.wordpress-org/icon-256x256.png differ diff --git a/.wordpress-org/screenshot-1.png b/.wordpress-org/screenshot-1.png new file mode 100644 index 00000000000..9b90d4fd05d Binary files /dev/null and b/.wordpress-org/screenshot-1.png differ diff --git a/.wordpress-org/screenshot-2.png b/.wordpress-org/screenshot-2.png new file mode 100644 index 00000000000..995273936b3 Binary files /dev/null and b/.wordpress-org/screenshot-2.png differ diff --git a/.wordpress-org/screenshot-3.png b/.wordpress-org/screenshot-3.png new file mode 100644 index 00000000000..84c39b4c4e3 Binary files /dev/null and b/.wordpress-org/screenshot-3.png differ diff --git a/.wordpress-org/screenshot-4.png b/.wordpress-org/screenshot-4.png new file mode 100644 index 00000000000..9865f3351bb Binary files /dev/null and b/.wordpress-org/screenshot-4.png differ diff --git a/.wordpress-org/screenshot-5.png b/.wordpress-org/screenshot-5.png new file mode 100644 index 00000000000..ed40c7f4743 Binary files /dev/null and b/.wordpress-org/screenshot-5.png differ diff --git a/.wordpress-org/screenshot-6.png b/.wordpress-org/screenshot-6.png new file mode 100644 index 00000000000..ba6dea591e0 Binary files /dev/null and b/.wordpress-org/screenshot-6.png differ diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 00000000000..3eeb1082ad3 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,2297 @@ +== Changelog == + += 3.1.0 - 2017-06-28 = +* Feature - Built-in product CSV importer and exporter for products. +* Feature - Display (toggle-able) terms inline on the checkout rather than showing a link. +* Feature - On the "pay for order" page, if logged out show a login form rather than an error message. +* Feature - Enabled oembed support for product short descriptions. +* Feature - Added bulk variation update for stock status. +* Feature - On customer profiles: added a button to copy billing address to shipping address. +* Feature - Setup Wizard - Automatic Shipping Zone Creation In Setup Wizard for the base location. +* Feature - Setup Wizard - Added a new optional Storefront Theme step if you're using a non-WooCommerce compatible theme. +* Feature - Made it possible to manage extension licenses purchased from WooCommerce.com on the extensions screen. +* Tweak - Gallery - Added a data-caption for captions to support both captions and titles for SEO. +* Tweak - Gallery - Used smoothHeight setting to better support images of different heights. +* Tweak - UI - Added blank states for API keys & webhooks. +* Tweak - UI - Made Product submenu labels consistent in admin. +* Tweak - UI - Changed street address field label and placeholder to minimize user error on checkout. +* Tweak - UI - Added a confirmation before deleting log files. +* Tweak - If prices are the same for all variations, use price not priceSpecification in structured data. +* Tweak - Added variable so shipping calculator is shown on first row only when showing multiple shipping packages. +* Tweak - Updated mini-cart HTML to use a list. +* Tweak - Allow linking to single product additional_information tab from url hash. +* Tweak - Re-included WooCommerce endpoints on the appearance > menus screens. +* Tweak - Always sync incorrect titles on variation read regardless of version. +* Tweak - Standardize rating HTML in all templates. +* Tweak - When searching, disable WC sort order so results are sorted by relevance. +* Tweak - Update price sorting code to use min or max for variable products depending on sorting direction. +* Tweak - Utilize $product method to get thumbnail in loops. +* Tweak - Check for an existing display name before updating a user on checkout. Adds display_name prop to the CRUD. +* Tweak - Adapt variable product price used in sorting based on direction of sort. +* Tweak - Made state validation less strict for keys. +* Tweak - For COD orders, force payment complete status to be completed. +* Fix - Use get_max_purchase_quantity in cart template and fix logic when stock management is off. +* Fix - Added log_id as the secondary sorting column to log list so log entries sort correctly. +* Fix - Fix shop page when using shop base and UTF8 shop page slug. +* Fix - Added handles so drag and drop does not break edit on mobile when sorting categories. +* Fix - Added ABSPATH checks to all files. +* Fix - Fixed how to flush rewrite rules after saving the shop main page. +* Fix - Emails sent via admin should switch to global locale. +* Fix - Set and restore wp_query so product page functions think it's a real product page. +* Fix - Variation default value of '0' fails to save on product. +* Fix - Prevent locations being added to the "Rest Of The World" shipping zone via the API. +* Dev - Allow date created to be set in wc_create_refund. +* Dev - Introduced a [WC_Order_Query class](https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query) for finding/searching orders. +* Dev - Added "restored" webhook. +* Dev - Support floats for the custom attribute name sorting function. +* Dev - Updated Emogrifier to version 1.2. +* Dev - Sort product data tabs by priority in admin screen. +* Dev - Added new hooks for: dashboard reviews widget, product and category sorting events, woocommerce_add_to_cart_sold_individually_found_in_cart, cart empty messages. +* Dev - Added filters for zoom / flexslider / photoswipe enabling. +* Dev - Added filter for cookie name. +* Dev - Added ability to filter Photoswipe lightbox options. +* Dev - Added new filter for product thumbnail size. +* Dev - Added action for displaying custom data for fees in admin. +* Dev - Changed build_payload from private to public in webhook system. +* Dev - Added deprecated notice to WC_Order_Item_Meta (deprecated in 3.0). +* Dev - Added namespace to jQuery events that are removed in VariationForm. +* Dev - Made WC_Checkout::get_posted_data() public. +* Dev - Add custom message for custom system status tools. +* Dev - Added filters to change which order items are created and loaded to support custom item types. +* Dev - Updated jQuery payment and serializejson libraries. +* Localization - Added Bolivian states. +* Localization - Use VAT for Norway instead of Tax. + += 3.0.9 - 2017-06-22 = +* Fix - Exclude sale products from category checks if coupon is not valid for sale products in coupon class. +* Fix - Fix missing states in state field when selected country differs from checkout data. Required template modification. +* Fix - Updated `woocommerce_email_actions` to send email when order status changes from processing to cancelled. +* Fix - Fix undefined variables in terms and legacy order API endpoints. +* Fix - Correctly update variation outofstock term on save. +* Fix - Add a nonce and confirmation message for logging out via the customer my account page. +* Fix - Allow setting grouped_products via the API. +* Fix - Prevent edge case errors in `wc_get_product_term_ids`. +* Fix - Remove extra escaping to fix saving of special characters in attribute terms. +* Fix - Stricter shipping method matching in COD to prevent conflicts. +* Fix - Recalculate totals after local pickup selection so taxes are recalculated. +* Fix - Add missing nonce to product sales report. +* Fix - Fix webhook save actions and ping the URL to test only once. +* Fix - Fix issue with CLI IDs which overlap with actual data. +* Fix - Normalise emails in coupons so lower/upper case is ignored. +* Fix - Added background color to `x` button in product gallery edit box. +* Dev - Renamed `woocommerce_credit_card_type_labels` filter from `wocommerce_credit_card_type_labels`. + += 3.0.8 - 2017-06-06 = +* Fix - Include multi-dimensional array support in oAuth1.0. +* Fix - Stock/backorder notice when stock management is disabled. +* Fix - Handle shipping item taxes if set to avoid the legacy fallback. +* Fix - Variations should inherit purchase_note from parent. +* Fix - Check if subtotal is blank, not empty, before setting for order items. +* Fix - Cancelled email should be send for processing orders, not pending. +* Fix - Missing variable in legacy API. +* Fix - Correct price query when on a post type archive. +* Fix - Missing $ip Variable in geolocation class. +* Fix - A single multi-word attribute is fine for variation titles. +* Fix - Gallery should be updated even if empty in REST API. +* Fix - Fix saving of text attributes with special chars. +* Fix - Undefined index warning when saving variations with stock management disabled. +* Fix - Use meta id instead of key in WC_Order_Item::offsetGet. +* Fix - Format parent stock qty on read. +* Fix - Hide replies from recent reviews widgets. +* Fix - Use formatted weight and dimensions for variations. +* Fix - Ensure we have child before getting price to fix a notice in grouped products. +* Fix - Fixed unicode characters when saving webhook delivery logs. +* Fix - Avoid deprecated ID in legacy API. +* Fix - Add correct args to woocommerce_shipping_zone_method_deleted and woocommerce_shortcode_products_query hooks. +* Fix - Correctly append cache in product widget. +* Fix - Add ability to invalidate cache by object ID. +* Fix - Notice in structured data class. +* Fix - Only delete if an object has an ID in CRUD to avoid wp_delete_post using global ID. +* Fix - Avoid notices on checkout by ensuring all legacy data is correctly set. +* Fix - Add failed to processing event for the processing email. +* Fix - Store user ID and use that to determine if the session should be loaded or not. Ensures user data is correct and shipping calculator data is stored. + += 3.0.7 - 2017-05-16 = +* Fix - Display of grouped product permalinks + names. +* Fix - Ensure `wc_get_payment_gateway_by_order` has a valid order ID to avoid errors. +* Fix - Ensure `get_plugin_updates` exists in API. +* Fix - Correctly set rating term after updating product visibility. +* Fix - `is_ip_address` should be static. +* Fix - Handle clearing for 3, 4, and 5 columns in the product gallery. +* Fix - Some added protection against notices/errors in the assets and variation data-store files. +* Fix - If backorders are enabled, do not make variable products out of stock. +* Fix - Undefined function in `class-wc-embed.php`. +* Fix - Fix 'base location' not being returned via the settings API. +* Fix - When re-filling fields on checkout, only change the empty ones to avoid conflicts with gateway plugins and hidden fields. +* Fix - Make calculate tax function clear taxes if taxes are disabled on recalculation. +* Fix - Update all customer session address fields when updating via checkout. +* Fix - Support customer searches < 3 characters long, but with result limiting. + += 3.0.6 - 2017-05-09 = +* Fix - Fixed conflict between global attributes and custom attributes with the same names. +* Fix - Added missing "id" to API for shipping zone methods to support the CLI. +* Fix - Incorrect use of `wc_format_price_range` in `get_price_html_from_to`. +* Fix - Clone each meta object when cloning WC_Data object to avoid modifying original meta stdClass objects. +* Fix - Fix non numeric warning for some order data. +* Fix - Fixed a warning when no customer country is defined for state input. +* Fix - Use term name when reordering so correct data is passed to the new order. +* Fix - Formatting issues in wc_display_item_meta. +* Fix - Check if IP address is valid in IP address detection code. +* Fix - wc_attribute_taxonomy_id_by_name should use wc_sanitize_taxonomy_name to prevent breaking special chars. +* Fix - Correct variable name in order structured data. +* Fix - Prepend new item keys with `$items_key` to make them unique. +* Fix - Hide offers from structured markup when blank. +* Fix - Fixed "Process to checkout" button color in Twenty seventeen dark theme. +* Fix - Only set reply-to if the email + name is set. +* Fix - Correctly exclude terms in wc_get_related_products. +* Fix - Reset post data prevents grouped products working in shortcodes. +* Fix - Fix min price range comparisons. +* Fix - Properly save order items in legacy REST API. +* Fix - Use correct full size for variation images. +* Fix - Add noscript style for gallery. +* Fix - Fix/duplicate potential stock reduction with paypal. +* Tweak - Improve _wc_term_recount performance. +* Tweak - Improve plugin update detection in system status report to reduce timeouts. +* Tweak - Improve "Save Order" button to reproduce WordPress post/page behavior. +* Tweak - Added zipcode validation for France. +* Dev - Added woocommerce_shop_order_search_results filter. + += 3.0.5 - 2017-04-28 = +* Fix - Tooltip display within shipping zone modals. +* Fix - Fix missing title for actions column on mobile. +* Fix - Allow forward slash in telephone field. +* Fix - Sort grouped products by menu order when displaying. +* Fix - Fix term exclusion in term count queries. +* Fix - Filter invalid products before returning them for wc_get_products. +* Fix - Prevent orders being their own parent (causes errors). +* Fix - Correctly migrate legacy shipping taxes data. +* Fix - Make sure the meta data cache is not shared among instances. +* Fix - Correct the stock display notice when a variable product manages stock for it's children. +* Fix - On multisite, add user to blog during checkout if not a user to prevent errors. +* Fix - Correct sale price date handling with some timezone setups. +* Fix - wc_attribute_taxonomy_id_by_name needs to use lowercase attribute slug to work. +* Fix - Make changes to the buyer's company name in the shipping section of checkout persist. +* Tweak - Add required placeholder for meta fields in backend. +* Tweak - Don't strtolower address strings on checkout validation messages. +* REST API - Prevent password change notification when creating a customer. +* REST API - Removed duplicated items returned in shipping and checkout endpoints. +* CLI - Fixed missing shipping zones route. +* Dev - Make get_price_html handling match 2.6 and pass all values through woocommerce_get_price_html filter. +* Dev - Legacy customer class missing get_address / get_address_2 functions. +* Dev - Restored filter `woocommerce_checkout_customer_id` during checkout validation. +* Dev - Adds missing `$this` argument for all `woocommerce_payment_complete_order_status` filters. + += 3.0.4 - 2017-04-20 = +* Fix - Variations were not inheriting the product image and shipping class ID. +* Fix - Prevent rating/review counts being duplicated when duplicating products. +* Fix - Fixed gallery navigation between images with long captions. +* Fix - Support transparent PNG in the gallery by setting a background color. +* Fix - Removed name/company from the shipping address Google map links. +* Fix - Fixed the address field sorting script on the checkout. +* Fix - Fixed the upgrade routine for grouped products so that parents are unset. +* Fix - Fixed support for WordPress 4.7 user locale settings. +* Fix - Fixed default option filter for product types in the product meta box. +* Fix - Improved the css in Twenty Seventeen for dark color schemes. +* Fix - Fixed display of refunds in sales report. +* Fix - Updated `single-product/add-to-cart/variable.php` template version to 3.0.0 since it had changes since 2.6. +* Fix - Fixed warnings when product attribute do not exists. +* Fix - Used a div for comment-form-rating to prevent invalid nested markup. +* Fix - Fixed some logic that checks if order taxes are compound. +* Fix - Fixed SKU checks to only exclude products that are trashed. +* Fix - Fixed display of download permissions in first email sent after checkout. +* Fix - Hidden the backorder notification stock text when notification is disabled. +* Fix - Fixed incorrect stock numbers in low stock emails. +* Tweak - Removed the non-functional order total input box, and combined the recalculation buttons into one working button. +* Tweak - Updated Guided Tour videos. +* Tweak - Updated js-cookie.js to 2.1.4. +* Tweak - Updated schema.org URLs to use HTTPS. +* Tweak - Status report request timeouts. +* REST API - Fixed an issue that prevented deleting a term if errors were thrown during creation. +* REST API - Fixed reports endpoint when querying by date. +* REST API - Fixed ignored order parameters when changing order status. +* Dev - Support guest orders in `wc_get_orders` function. +* Dev - Fixed downloadable variation filters for download URLs. +* Dev - Added safeguards to prevent infinite loops while saving coupons, products and orders in admin. +* Dev - Added a fallback for `queue_transactional_email` if background sending is disabled. +* Dev - Added `has_shipping_address` helper method. +* Dev - Introduced `woocommerce_order_item_get_formatted_meta_data` filter. +* Dev - Made wc_add_order_item pass correct values to woocommerce_new_order_item. +* Dev - Fixed `legacy_posted_data` access in checkout class. +* Dev - Fixed undefined property notice in `WC_Order_Item::offsetGet`. +* Dev - Fixed PHP 7.1 warnings when using non-float values to `wc_get_weight()`. +* Dev - Fixed incorrect variable name in `wc_add_order_item()`. + += 3.0.3 - 2017-04-13 = +* Fix - Fixed an issue with variation tax-classes when set to 'parent'. This made taxes apply on top of the tax inclusive price in certain setups. +* Fix - Escaped attribute translations in the `cart.php` template and bumped the template version to match. +* Fix - Corrected the display of refund dates on the order screen. +* Fix - Fixed the grouped product visibility check in the grouped.php template and bumped the template version to match. +* Fix - Fixed the sale badge display for grouped products. +* Fix - Added the `itemReviewed` structured data for product reviews to make it validate. +* Fix - Made the `get_attribute` method work on variation objects. +* Tweak - Turned off the deferred email sending by default which was added in 3.0. Whilst it does improve performance, there were compatibility problems on some servers. It can be enabled with a filter if desired. +* Dev - Added backtrace information to the deprecation messages to help find problem plugins. + += 3.0.2 - 2017-04-12 = +* Fix - Removed required states for GP, GF, KW, LB, MQ, RE and YT countries. +* Fix - Made cache in the [products] shortcode respect filters from plugins. +* Fix - Added missing `woocommerce_cross_sells_columns` filter. +* Fix - Fixed shortcode rendering on the shop page. +* Fix - Fixed incorrect sale dates when bulk editing variations. +* Fix - Fixed calls to wc_reduce_stock_levels in PayPal and Simplify gateways. +* Fix - Exclude "location" meta when reading customer meta data. +* Fix - Updated `emails/email-addresses.php`, `emails/email-order-details.php`, `content-single-product.php`, `checkout/form-shipping.php`, `myaccount/form-add-payment-method.php`, `myaccount/form-edit-address.php`, `myaccount/form-lost-password.php`, `myaccount/form-reset-password.php`, `myaccount/orders.php` and `myaccount/view-order.php` template version to 3.0.0 since they had changes since 2.6. +* Fix - Fixed default behavior of variation tax classes when originally set to "parent". +* Fix - When duplicating products, do not copy slug, append "(Copy)" to the product name, correctly copy all meta data, and prevent children of grouped products being duplicated too. +* Fix - Removed duplicated items when outputting cross sells on the cart page. +* Fix - Fixed output of default "add to cart" text of external products in loops. +* Fix - Fixed backwards compatibility of guest checkout rules when being altered by plugins directly. +* Fix - Use correct thumbnail sizes for variation images in the new gallery. +* Fix - Fixed captions on thumbnails and main image in the new gallery. +* Fix - Trigger wc_fragments_loaded after add to cart fragment refresh. +* Fix - Download permissions; Convert dates to timestamp on read so UTC is preserved. +* Fix - Fixed notices under PHP 7.1 when sorting products by name (numeric). +* Fix - Added additional checks to ensure objects are read before using class methods to avoid errors. +* Fix - Removed legacy suggest.js code which was causing JS error on bulk edit. +* Fix - Fixed warnings on the "Lost password" page and when loading a product with invalid attributes. +* Fix - Made background emailer update the queue after a successful send so duplicate mails are less likely. +* Fix - Typo in flexslider_enabled option in new gallery script. +* Fix - woocommerce_notify_low_stock and woocommerce_notify_no_stock options had no effect. +* Tweak - For downloadable files, only validate file type when dealing with relative paths. +* Tweak - Improved automatic variation name generation. +* Dev - Added product visibility terms to system status report to help debug. +* Dev - Introduced `woocommerce_admin_order_date_format` filter to replace missing `post_date_column_time`. +* Dev - Introduced `woocommerce_update_customer_args` filter to prevent updates to user objects if needed. +* REST API - Fixed saving of variations in legacy REST API v3. +* REST API - Fixed backwards compatibility of line_items meta in legacy REST API. + += 3.0.1 - 2017-04-06 = +* Fix - Show catalog hidden products within grouped products. +* Fix - Fade in the gallery in if no images are set or it's custom. +* Fix - Use wc_deprecated_function in WC_Deprecated_Hooks so notices aren't output in ajax requests. +* Fix - Added back the ability to include extra items to the System Status using the `woocommerce_system_status_environment_rows` filter. +* Fix - Coupon category restrictions and limits for variations. +* Fix - Allow shortcodes and HTML in variation descriptions like in 2.6. +* Fix - Unset post date when duplicating products. +* Fix - Show a sale price on variable products if on sale and all prices are the same. +* Fix - Corrected download links when a product has multiple downloads. +* Fix - Prevented potential errors if the product type was not posted for any reason on save. +* Fix - Updated `single-product/up-sells.php`, `loop/add-to-cart.php`, `loop/rating.php`, `checkout/form-billing.php`, and `content-product.php` template version to 3.0.0. +* Fix - Included clearfixes on billing and shipping field wrappers, +* Fix - Fixed styling of logs table in some languages. +* Fix - Fixed display of variation attributes on old orders. +* Fix - Use placeholder text for external products add to cart button text if left blank. +* Fix - Fallback to home URL if no shop page is set for system status security check for HTTPS. +* Fix - For variations, pull tax status and sold individually from the parent since there is no UI to set this at variation level. +* Fix - Moved cron emails to background processing to avoid multiple sends. +* Fix - Wrapped structured data in a hidden element when added to emails. +* Fix - Missing gateway information in queued emails. +* Fix - Fixed a bug that caused pages to permanently reload if "Default customer location" was set to "Geolocate (with page caching support)". +* Fix - When forcing shipping to billing, set the shipping fields in the order itself. +* Fix - Check for invalid objects in WC_Register_WP_Admin_Settings. +* Fix - Check for error object in wc_get_object_terms. +* Fix - Removed slashes in shipping meta data on the order edit screen. +* Fix - Prevented permalink rewrites for attributes with missing names. +* Fix - Fixed saving of meta data when multiple extensions use the `save_post` action. +* Fix - Allow search customers by ID in edit order screen. +* Fix - Prevents session data overwriting customer data on login. +* Fix - Fixed cross-sell column display and variation support. +* Fix - Fixed variable product stock syncing on save. +* Fix - Included try/catch wrapper to prevent issues with Select2. +* Fix - Prevented a bug that deleted all variations when the product type was change from variable to simple. +* Fix - Switched to WPDB to quicker update when syncing titles for variations. +* Fix - Exclude deprecated properties when loading a customer object. +* Fix - Fixed notices while trying to order again. +* Fix - Fixed notices when `$wpdb->prefix` is empty. +* Fix - Prevent errors when loading a product with an invalid download file types. +* REST API - Fixed missing array declaration in CRUD controller. +* REST API - Removed extra `exclude`, `include` and `search` parameters from taxes endpoint. +* REST API - Fixed variation description formatting. +* REST API - Fixed incorrect attribute check in products endpoint in Legacy REST API. +* REST API - Allow variation image to be unset. + += 3.0.0 - 2017-04-04 = +* New gallery on single product pages with better mobile support, using PhotoSwipe and Zoom. Declare support with add_theme_support() - wc-product-gallery-zoom, wc-product-gallery-lightbox, wc-product-gallery-slider +* Made the store notice dismissible on the frontend. +* Variable products no longer show striked out prices in combination with ranges for clarity when on sale. +* Prices no longer display as 'free' instead of 0, to fix issues with ranges and localization and for consistency. +* Improved structured product data by using JSON-LD instead of inline Microdata. +* Improved downloads list layout (template file). +* Respect stock status and prevent the "out of stock threshold" setting affecting existing in-stock products. +* Improved handling of shop page rewrite rules to allow subpages. +* Redirect to login after password reset. +* When using authorizations in PayPal Standard, automatically capture funds when the order goes processing/completed. +* On multisite, when a user logs into a store with an account on a site, but not the current site, rather than error, add the user to the current site as a customer. +* Show variable weights/dimensions even when parent values are not set. +* Automatically sort tax rates rather than allow clunky manual sorting. +* When deleting a tax rate class, remove it's tax rates. +* Made WC_Logger pluggable via wc_get_logger function. +* Use 'average rating' post meta for 'rating' product sorting option. +* Show better labels in nav menus metabox. +* Sort “Recently Viewed” products by the view order. +* Removed internal scroll from log viewer. +* Add reply-to to admin emails. +* Improved the zone setup flow. +* Made wc_get_wildcard_postcodes return the orignal postcode plus * since wildcards should match empty strings too. +* Use all paid statuses in $customer->get_total_spent(). +* Move location of billing email field to work with password managers. +* Option to restrict selling locations by country. +* Added tool to clear orphaned variations in system status. +* Remove checkbox options in system status tools and replace with constants. +* Added security section in system status report. +* Add image_url setting to PayPal Standard. +* Fixed attribute registration. Attributes are non-hierarchical by default (parent is not supported). +* Add sort parameter to checkout fields to aid with sorting per locale. +* Merged percent and percent product coupon types (they provide the same discount). +* Prevent payment details being cleared after update_checkout events. +* Performance - Converted _featured and _visibility meta data to terms for faster catalog queries. Upgrade routine handles migration. Developers may need to update queries to reflect this change. +* Includes product attributes archives links in "Additional Information" tab. +* Select2 has been upgraded to v4. +* Improved logging system for extensions. +* Tax suffix is now hidden on non-taxable products. +* Grouped products are linked from the parent rather than the children. Children can be in more than one group. +* Removed coupon usage link in coupons admin screen. +* Performance - Converted rating filters to visibility terms. +* Performance - Added visibility term for outofstock products to speed those queries up also. +* Performance - Introduced a new CRUD (create, read, update, delete) system for Products, Orders, Customers and Shipping Zones. +* Performance - Optimised variable product sync. Upper/lower price meta is no longer stored, just the main prices, if a child has weight, and if a child has dimensions. +* Performance - Removed WP_Query from up-sells.php and related.php and replaced with PHP foreach loop (since we already have the product IDs). +* Performance - Removed the feature where old orders get access to new downloads on product edit. Looping potentially thousands of orders to do this is too much of a performance burden for stores and this can sometimes be unexpected behavior too. This does however updates *edited* downloads. +* Performance - Removed 'items' column on orders screen due to loading excessive data. +* Performance - Deferred email sending for faster checkouts. Now uses CRON. +* API - New Rest API v2 with support for meta_data amongst other things. +* API - Removed last order from customers part of the API due to performance concerns - use orders endpoint instead. Other order data on the endpoint is now transient cached. +* API - Allow oAuth1.0a authentication using headers. +* API - New Shipping Zones endpoints. +* API - New variations endpoints. +* API - New settings endpoints. +* API - Payment gateways and shipping methods endpoints. +* API - Prevented the (broken) ability to manipulate variations directly on the products endpoints. +* CLI - New CLI which uses the REST API endpoints rather than it's own functions. +* Localization - Improved RTL support. +* Localization - Added a language independent permalink setting function. +* Localization - Added inline comments for placeholder strings. +* Localization - Added Nigerian and Pakistan Provinces to i18n/state. +* Localization - US and Poland postcode validation. +* To read more about this release, see our dev blog announcement here: http://wp.me/p6wtcw-Uo + += 2.6.14 - 2017-02-02 = +* Fix - Ensure product exists in wc_update_product_stock. +* Fix - Send emails using the site language. +* Fix - Remove tilde typo. +* Fix - Fixed notice in get_rating_count. +* Tweak - Define arg and return data types, added extra descriptions, and correctly cast IDs in the Rest API. +* Tweak - Handle custom error data in WC_REST_Exception. +* Tweak - Display conflicted product ID when using a duplicate SKU via the API. +* Localization - Add Finnish defaults to the installer. + += 2.6.13 - 2017-01-18 = +* Fix - Demo store banner styling in 2017. +* Fix - Removed default instructions from COD, BACS and Check gateways so displayed messages can be unset. +* Fix - Made variation options update on first load. +* Localization - Added Romanian locale to the installer. + += 2.6.12 - 2017-01-12 = +* Fix - Make images shown up on pageload when using ajax variations. +* Fix - Allow variations options to be deselected in IE11. +* Fix - Disabled-button and pagination styling in 2017. +* Fix - PHP 7.1 compatibility issues with non-numeric math operations. +* Fix - Fix notices in abstract class when price is empty. + += 2.6.11 - 2016-12-22 = +* Fix - Variation form compatibility with quotes in attribute values, and initial variation image fadeIn on certain configs. + += 2.6.10 - 2016-12-22 = +* Fix - Flat rate no class costs when no shipping classes exist. +* Fix - Returned REST API coupon expiry date. +* Fix - reviews_allowed being set to false in Rest API. +* Fix - Sales date series for some custom ranges. +* Fix - Missing attributes when an option is chosen by default on variations. This was the result of a Firefox 50 compatibility fix. In order to support both Firefox, Chrome, IE, and Edge we've done some refactoring of the variation add to cart scripts. +* Tweak - Updated Geo IP API services. +* Dev - Added support for WP VIP/VIP GO GEO IP headers. +* Dev - API - Throw error messages when product image ID is not a valid WordPress attachment ID. + += 2.6.9 - 2016-12-07 = +* Theme - Added support for Twenty Seventeen Theme. +* Fix - Excluded webhook delivery logs from comments count. +* Fix - Included password strength meter in "Lost Password" page. +* Fix - Order fee currency in admin screen. +* Fix - Variation selection on Firefox 40. +* Fix - Don't prevent submission when table is not found on cart. +* Fix - Improved layered nav counts on attribute archives. +* Fix - Fixed pagination when removing layered nav items via widget. +* Fix - Default BE tax rate. +* Fix - Downloads should store variation ID rather than product if set. Also fixes link on account page. +* Fix - Use wp_list_sort instead of _usort_terms_by_ID to be compatible with 4.7. +* Fix - Only return empty string if empty for weight and dimension functions. +* Fix - Added correct fallbacks for logout/lost password URLs when endpoints are not defined. +* Security - Wrapped admin tax rate table values in _escape to thwart evil CSVs an admin user could upload. Vulnerability was discovered by Fortinet’s FortiGuard Labs. +* Dev - API - Only update categories menu order and display if defined. +* Dev - Fixed when should deliver wp_trash_post webhooks. + += 2.6.8 - 2016-11-10 = +* Fix - REQUEST_URI was missing a trailing slash when being compared in the cache prevention functions. +* Fix - Prevent issues when sending empty prices to PayPal. +* Fix - Invalid email check. +* Tweak - New extensions screen. + += 2.6.7 - 2016-10-26 = +* Fix - Use FLOOR and CEIL to get price filter values. Fixes the issue where max price is capped at 99. +* Fix - Hide "Sales this month" information from Dashboard widget for users that don't have `view_woocommerce_reports` capability. +* Fix - Remove notices only once on cart so subsequent notices do not remove older notices. +* Tweak - Improve credit card fields for better mobile experience. + += 2.6.6 - 2016-10-20 = +* Fix - Conflict with Local Pickup Plus extension due to 2.7.x code in has_shipping_method(). +* Fix - Shipping method display order on frontend. + += 2.6.5 - 2016-10-19 = +* Fix - Shipping classes URL in admin. +* Fix - Notice in reports when using custom date ranges. +* Fix - When checking needs_shipping, ignore anything after : in the method ID. +* Fix - Allow has_shipping_method to work with instances. +* Fix - Potential notice in wc_add_to_cart_message(). +* Fix - Prevent notice in wpdb_table_fix if termmeta table is not used. +* Fix - Payment method box fixes e.g. maintain previously selected payment method after update. +* Fix - Prevent multiple password validation methods at once on my account page. +* Fix - Ship to specific counties option had no effect. +* Fix - Broken Webhook delivery due to use of post_date_gmt which does not exist for drafts. +* Fix - Use method title in admin shipping dropdown. +* Fix - Fixed downloadable variable product URL. +* Fix - Handle object when generate_cart_id is used to prevent notices. +* Fix - Set header link color in emails. +* Fix - Rest of the world ID 0 zone handling when using CRUD classes. +* Fix - Cast prices as decimal when querying prices in price filter widget. +* Fix - API - Fix coupon description field. +* Fix - API - ID needs to be capitalized to allow correct sorting. +* Fix - API - Fixed undefined order ID. +* Fix - API - Allow API to save refund reason. +* Fix - API - Resolved encoding issues with attribute and variation slugs. +* Fix - API - get_attributes should return term name, not slug. +* Fix - API - Product "filter" and "sku" parameters. +* Fix - Handle info notices in cart, not just error messages. +* Fix - Don't remove hyphens in attribute labels. +* Fix - Start sales on variations after they are saved, if applicable. +* Fix - Made the text showing max variations you can link match the actual filtered value. +* Fix - Add missing tables to wpmu_drop_tables function. +* Fix - When syncing variation stock, ensure post is a variation. +* Fix - Resolved some sales by date sum issues. +* Fix - Fix cart update in IE when enter key is pressed. +* Fix - Variation is_on_backorder when parent manages stock. +* Fix - Fix variation script malfunctioning when show_option_none arg is set to false. +* Fix - Fire tokenisation event on load for pay page. +* Fix - Populate attribute dropdown when empty. +* Fix - Fix email check on my account page. +* Fix - Send processing email on on-hold to processing transition. +* Fix - Incompatibility with SQLite databases. +* Fix - KGS and ISK currency symbols. +* Tweak - Password reset now uses WP functions. +* Tweak - Format US 9-digit postcodes. + += 2.6.4 - 2016-07-26 = +* Fix - Security - Only allow image MIME type upload via REST APIs. +* Fix - Shipping method title display in COD settings. +* Fix - Order date input in Edge browser. +* Fix - Ensure value is not null in variations to support empty show_option_none setting. +* Fix - get_the_title does not need escape in grouped template file. +* Fix - Ensure WC_ROUNDING_PRECISION is defined and use it as a low precision boundary in wc_get_rounding_precision(). +* Fix - Response body should be a string in webhook class. +* Fix - Use h2 instead of h3 headings in profile screen. +* Dev - API - Allow Allow meta_key/value filters for products. +* Dev - CLI - Explode tags and category IDs to allow multiple comma separated values. +* Dev - add $order arg to woocommerce_admin_order_item_class and woocommerce_admin_html_order_item_class filters. + += 2.6.3 - 2016-07-19 = +* Fix - Security - Escape captions in product-thumbnail and product-image templates (template versions have been bumped). +* Fix - Fixed how we calculate shipping tax rates when using more than one tax class. +* Fix - When duplicating product variations, set title, name, and guid. +* Fix - Normalized 'read more' buttons. +* Fix - Add to cart notices for grouped products. +* Fix - Do not sanitize passwords in the settings API. +* Fix - Handle shipping zone location range conversion during update (dashes to ...). +* Fix - Always remove commas while processing flat rate costs. +* Fix - Ensures account page layout is only applied to desktop-sized displays. +* Fix - When getting layered nav counts, take search parameters into consideration. +* Fix - Free shipping show/hide javascript. +* Fix - Strip hash characters when exporting reports. +* Fix - Use permission id to revoke access to downloads to prevent removing wrong rows. +* Fix - When duplicating product variations, set title, name, and guid. +* Fix - Set more appropriate default rounding precision based on currency decimal places. +* Fix - Fix message styles for empty carts. +* Fix - Fixed the load of the WC_Email_Customer_On_Hold_Order class. +* Fix - Don't perform cart update on search submit. +* Dev - API - Added support for WP REST API with custom URL prefixes. +* Dev - API - Delete variations when deleting a variable product. +* Dev - API - Fixed how we check for product types. +* Dev - Added woocommerce_cart_id filter. +* Dev - Add shortcode name param to shortcode_atts function calls. +* Dev - Post custom data when fetching a variation via ajax. +* Dev - Include child prices in grouped_price_html filter. +* Dev - Allow filtering of variation stock quantity. +* Dev - Added $_product argument to 'woocommerce_restock_refunded_item' hook. +* Dev - Added a filter hook for the wc_ajax endpoint url. +* Tweak - Include account page link in new customer account emails. +* Tweak - Updated all URLs from WooThemes.com to WooCommerce.com. +* Tweak - Cache the result of WC_Comments::wp_count_comments() in a transient (improves performance). + += 2.6.2 - 2016-06-30 = +* Fix - Set max index length on woocommerce_payment_tokenmeta table for utf8mb4 support. +* Fix - is_available check for legacy shipping methods. +* Fix - wc_add_to_cart_message() when non-array is passed. +* Fix - Maximum coupon check should allow the 'maximum' value. +* Fix - Product coupon logic to avoid applying non-applicable coupons. +* Fix - Potential notices when leaving out 'default' field for shipping instances. +* Fix - wp_cache_flush after term meta migration/update. +* Fix - wc_add_to_cart_message() when non-array is passed. +* Fix - woocommerce_redirect_single_search_result type check was incorrect. +* Fix - Javascript show/hide of option in free shipping method. +* Fix - Convert ellipsis to three periods when saving postcodes. +* Fix - Prevent get_terms returning duplicates. +* Fix - Removed non-existent country (Netherlands Antilles) from https://en.wikipedia.org/wiki/ISO_3166-1. +* Fix - Grouped product range display when child is free. +* Fix - Remove discount when checking free shipping min amount. +* Fix - Prevent blocking the same element multiple times on cart page. +* Fix - Don't sync ratings right after a new comment to prevent rating sync whilst rating meta does not exist yet. +* Fix - Fix product RSS feeds when using shop base. +* Fix - woocommerce_local_pickup_methods comparison by stripping instance IDs before the check. +* Fix - During password resets, use cookie to store reset key and user login to avoid them being exposed in the URL/referer headers. +* Dev - API - Fixed variable product stock at product level. +* Dev - CLI - Introduces `woocommerce_cli_get_product_variable_types` filter. +* Dev - Allow notices to be grouped on checkout after certain events. +* Dev - API - Allows save images by ID with product variations. +* Tweak - Made customer pay link display if order needs_payment() rather than checking pending status. +* Tweak - Zones - Wording clarifications. +* Tweak - Zones - Match zones with postcodes but no country. +* Tweak - Zones - Match zones with no regions as 'everywhere'. +* Tweak - Added view_admin_dashboard cap for disabling the admin access restriction in custom roles. +* Tweak - Revised stock display based on feedback to hide 'in stock' message if stock management is off and only show available on backorder if notifying customer. +* Tweak - Allow external product SKUs. +* Tweak - PT (Portugal) and JP (Japan) postcode formats. +* Tweak - Sort products from the `[product_category]` shortcode by menu order. +* Tweak - Improve wc_orders_count() performance by running a query to count only posts of the given status. +* Tweak - To allow my account page tabs to be disabled without code, you can now set the endpoint value to a blank string. + += 2.6.1 - 2016-06-16 = +* Fix - Added missing localized format for line taxes in orders screen to prevent total miscalculation in manual orders. +* Fix - Improved the hour and time fields validation pattern on the orders screen. +* Fix - PayPal does not allow free products, but paid shipping. Workaround by sending shipping as a line item if it is the only cost. +* Fix - SKUs prop on products shortcode. +* Fix - Layered nav counts when term_id does not match term_taxonomy_id (before splitting). +* Fix - Fixed referer links from cart messages in WP 4.4. +* Fix - Fix the showing/hiding of panels when terms do not exist by using wc_get_product_types() for retrieving product types. +* Dev - content-product.php and content-product_cat.php contained the wrong version. +* Dev - Show "matching zone" notice on the frontend when shipping debug mode is on. +* Dev - Restored missing WC_Settings_API::init_form_fields() method to prevent potential errors in 3rd party gateways. +* Dev - API - Fixed returned data from product images (changed `title` to `name`). +* Dev - API - Fixed products schema for `grouped_products`. +* Dev - API - Fixed products attribute options when contains `,`. +* Tweak - Hide 'payment methods' screen if no methods support it. +* Tweak - If shipping method count changes, reset to default. +* Tweak - Avoid normalization of zone postcodes so wildcard matching can be performed on postcodes with spaces. E.g. SP1 * +* Tweak - Allow max_fee in addition to min_fee in flat rate costs fields. +* Tweak - Wrap order_schema_markup() output in hidden div in case script tag is stripped. + += 2.6.0 - 2016-06-14 = +* Feature - Introduced Shipping Zone functionality, and re-usable instance based shipping methods. +* Feature - Tabbed "My Account" area. +* Feature - Cart operations now use ajax (item quantities/remove, coupon apply/remove, shipping options). +* Feature - Layered nav; filter by rating. +* Feature - On-hold order emails. +* Dev - All new REST API based on the WP REST API. The old WC REST API is still available, but the new one is preferred. +* Dev - Added ability for shipping methods to store meta data to the order. +* Dev - Added Payment Gateway Tokenization API for storing and retrieving tokens in a standardized manner. +* Dev - Migrated custom term meta implementation to WP Term Meta. +* Dev - Added new wc_get_orders() function to get order objects and ids instead of direct get_posts() calls. +* Dev - Made coupon optional in cart has_discount() method. +* Dev - Made the review template more editable. +* Dev - Allowed product constructors to throw exceptions if invalid. +* Dev - Wrapped currency symbols in a span to allow styling or replacement. +* Fix - Update download permission user and email when changed. +* Fix - Fixed shipping method unregistration. +* Fix - Stopped create and update webhooks firing at the same time for products. +* Fix - Allow COD to set on-hold status if the order contains downloads. +* Fix - Force CURL to use TLS 1.2 for PayPal connections. +* Tweak - Improved lost password flow. +* Tweak - Show payment dates on order screen. +* Tweak - Ignore catalog visibility on products shortcode when specifying IDs or SKUs. +* Tweak - Added context to checkout error messages. +* Tweak - Added SKU field to grouped products. +* Tweak - Moved SKU field to inventory tab. +* Tweak - Support qty display in cart messages. +* Tweak - Hide min order amount field when not needed in shipping settings. +* Tweak - If shipping < 999.99, use 'shipping' arg when passing values to PayPal. +* Tweak - Show net sales on dashboard. +* Tweak - Replaced credit card icons with SVG. +* Tweak - Enqueue scripts on pages with checkout shortcodes. +* Tweak - Color code the manual, system and customer notes. +* Tweak - Layered Nav Refactoring to improve performance. +* Tweak - Removed tag/cat classes from loops since WP does the same. +* Tweak - Added hash check for orders so that if the cart changes before payment, a new order is made. +* Tweak - Removed unused 'view mode' under screen options. +* Tweak - Added 110 new currencies (including Bitcoin). +* Tweak - New background updater for data upgrades. +* Tweak - Blank slates in admin post screens. +* Tweak - Added blockui when variations are being retrieved via ajax. +* Tweak - Hide empty taxes by default (filterable). +* Tweak - Allow failed orders to be edited. +* Tweak - If there are no shipping methods setup, don’t prompt for shipping at checkout. +* Tweak - Allowed country exclusion, rather than just inclusion, in ‘sell to’ setting. +* Lots, lots more - [see the comparison here](https://github.com/woocommerce/woocommerce/compare/2.5.5...2.6.0). + += 2.5.5 - 2016-03-11 = +* Fix - Before running dbdelta, drop indexes to prevent duplicate key notices. +* Fix - Prevent notice when unsetting terms on product edit screen. +* Tweak - zeroclipboard fallback for firefox on system status report. +* Tweak - Check valid product ID is provided on add_to_cart shortcode. + += 2.5.4 - 2016-03-10 = +* Fix - Fix table creation when using utf8mb4 charset. +* Fix - Have wp_insert_post return WP_Error when creating our coupon, so the is_wp_error check can catch it. +* Fix - Clear sale price on save if sale is no longer valid. +* Fix - Round refund values to ensure refunds can be performed. +* Fix - When getting coupon by code used twice, latest should be queried. +* Fix - CLI improvements for setting up variations and deleting orders. +* Fix - Allow big selects when getting variations to support larger queries. +* Fix - Trigger webhook when user edits addresses on frontend. +* Fix - Hide shipping row when calculator is disabled, and shipping costs are hidden. +* Fix - Unset deleted attributes when updating products. +* Tweak - Update date for paid orders during non-manual updates only. +* Tweak - wc_get_page_permalink - if the page ID is not set, redirect home instead to prevent white screens. +* Tweak - Remove log dir from system status report. +* Tweak - When sorting by date, fallback to ID. +* Tweak - Rename pay link for clarity. +* Tweak - Provide a fallback message if copying to the clipboard fails in system status report. + += 2.5.3 - 2016-03-01 = +* Fix - Correct the 'unavailable template' call for variations so the message is displayed correctly, fixing a JS error. +* Fix - Add 'media-models' dependency to write panel scripts. +* Fix - Fix hide empty check in category walkers. +* Fix - Current class fix on some servers when empty. +* Fix - Multibyte safe trim string function. +* Fix - Prevent a notice by stopping a loop in woocommerce_products_will_display from stomping on other variables. +* Fix - If an attribute meta key is not set, technically its 'any', so should match. Prevents issues when meta data is missing after renaming attributes. +* Fix - Make wc_get_product_variation_attributes ignore non variation attributes. +* Fix - Notice when no order notes exist. +* Fix - Removed extra tab from plain email shipping address. +* Fix - Round shipping after tax calculation instead of before to prevent wrong taxes being calculated. +* Fix - State input box was not reappearing when switching from a hidden input to a text input. +* Fix - Don't duplicate rating and review counts. +* Fix - CLI - Allow setting of a single category. +* Fix - API - Replace term_taxonomy_id for term_id whilst creating/editing terms. +* Fix - API - Fix parent_id and menu_order for variations. +* Fix - Combine update post calls when update_status is ran. +* Fix - Total number of comments in the admin panel. +* Tweak - Show customer details for logged in users only on thanks page to prevent customer details being revealed if someone finds out the URL. +* Tweak - Wrap status report in backticks to stop people breaking .org forums. +* Tweak - Error handling for screen ids. +* Tweak - Use $wpdb->replace instead of doing a select and then deciding to do an update or insert in session handler. +* Tweak - Added check for private WooCommerce pages in status report. +* Tweak - Transactional emails for failed -> on hold. +* Dev - Include new triggers when removing and adding the password strength meter. +* Dev - Allow pass objects and arrays as webhook callbacks. + += 2.5.2 - 2016-02-01 = +* Fix - Compatibility with w3 total cache inline minification. +* Fix - Remove stock bw compat code which was preventing manage stock being disabled at variation level. +* Fix - When calculating shipping total, force rounding. +* Fix - Make save button clickable in tax rate table after using autocomplete field. +* Fix - Fix passed image_size variable in email templates. +* Fix - Don't show purchase note to admin in emails. +* Fix - Fix 'hide empty' setting in category widget
. +* Fix - Prevent notice in get_allowed_countries. +* Fix - Prevent add-to-cart querystring in pagination links. +* Tweak - Allow propagation in variation script. +* Tweak - Product image alt text. +* Tweak - Remove notice and add styling for add payment page. +* Tweak - Set input margin and label display for compatibility with themes using bootstrap CSS. +* Tweak - Add context to category term localization. +* Tweak - Moved cart URL functions to core-functions file to make them available in admin area. +* Tweak - Added password hint text and error messages when showing the password strength meter in forms. +* Tweak - Added Saudi Riyal currency. +* Tweak - Added Russian Ruble symbol. +* Tweak - When COOKIEPATH is an empty string, set to '/' so cookies work across all pages. +* Dev - Template - Pass $category into wc_product_cat_class() in content-product_cat.php + += 2.5.1 - 2016-01-25 = +* Fix - Remove usage of get_currentuserinfo() which is deprecated in WordPress 4.5. +* Fix - Fix responsive product sizes when the columns class is missing. +* Fix - Fix function exists check for woocommerce_template_loop_category_title. +* Fix - check_version on all requests so that the installer runs after remote plugin updates. +* Fix - Only show the "add payment method" button when needed, and check for required fields on the add payment method page. +* Fix - Correctly block UI to prevent attribute issues in backend when adding multiple attributes in quick succession. +* Fix - Show SKU in admin emails. +* Fix - Don't show downloads in admin emails. +* Fix - Fix query/missing variable in validate_user_usage_limit function. +* Fix - Prevent endless loading on checkout when reload_checkout session variable was used. +* Fix - Correctly display html entities in tax screen autocomplete. +* Fix - Do sales reports based on refund line items rather than fully refunded orders to prevent double refunds being reported. +* Fix - Qty button can be hidden for variable products sold individually. +* Fix - Show the taxable country rather than base country in "estimated for" text during checkout. +* Fix - Prevent select2 gaining focus on IOS7 scroll. +* Fix - API - Fix indexes on decimal and thousand values. +* Tweak - Clear cron jobs on uninstall
. +* Tweak - Don't disable place order button on checkout if a weak password is used. +* Tweak - Added password strength meter in lost password and edit accout pages. +* Tweak - Pass $args to woocommerce_dropdown_variation_attribute_options_html hook. + += 2.5.0 - 2016-01-18 = +* Feature - New default session handler. Uses custom table to store data rather than the options table for performance and scalability reasons. https://woocommerce.wordpress.com/2015/10/07/new-session-handler-in-2-5/ +* Feature - New tax settings UI - faster, enhanced with ajax, searchable. +* Feature - WP CLI Support. https://woocommerce.wordpress.com/2015/10/01/sneak-peek-wp-cli-support-in-woocommerce/ +* Feature - Added terms and conditions checkbox to pay page. +* Feature - Password strength indicators. +* Feature - Added 'pay' link to order screen. +* Feature - Added admin order/payment failed notification. +* Fix - Check for existence of global attribute when you get_attributes() for a product. +* Fix - Show order by template on product search. +* Fix - Search variation skus in backend search. +* Tweak - For coupons with category restrictions, respect the category hierarchy. +* Tweak - Added wc_array_cartesian function to generate variations in a logical order. +* Tweak - Revised email settings screens to show emails in a table and avoid a long sub-nav. +* Tweak - Default customer role capabilities. +* Tweak - Expire mini-cart cache after 24 hours. +* Tweak - Improved refund error messages in PayPal Standard. +* Tweak - Removed language pack downloader in favour of translate.wordpress.org. +* Tweak - Added onboarding wizard button to the contextual help so it can be accessed again. +* Tweak - When a WordPress user is deleted, turn any orders they have into Guest orders. +* Tweak - When calculating order taxes, respect tax settings and default to base country. +* Tweak - Fade in variation images to avoid flicker during load. +* Tweak - Display 2 averages on report (net and gross). +* Tweak - Improve product search and use WPDB instead of several get_posts queries for performance. +* Tweak - Use SKU for stock order notes. +* Tweak - Added order notes for manual email sends. +* Tweak - Sanitize shipping method labels/titles. +* Tweak - Only display the coupon form on the checkout if a coupon hasn't been applied. +* Tweak - Added billing address column to order screen (off for new users). +* Tweak - Created function to disable author archives for customers. +* Tweak - When updating cart hash, refresh all open tabs. +* Tweak - Use new "question" mark icon font for help tips. +* Tweak - Improved review verification status retrieval. +* Tweak - Improve appearance when only 1 gateway is active. +* Tweak - Aligned terms box left and added required asterisk. +* Tweak - Removed dropdown display mode for cart shipping methods - radios are more flexible. +* Dev - API - Added /products/shipping_classes endpoint. +* Dev - API - Added support to POST, PUT, and DELETE categories and tags. +* Dev - API - Added support to filter products by tag, category, shipping class, and attribute. +* Dev - API - Added tax and tax_class endpoints. +* Dev - Template - New star ratings. The old one was 5 separate buttons. This new one consolidates the 5 options into one element making it leaner visually and more intuitive. Works in IE9+ with a graceful degradation for IE8. +* Dev - Template - Added `data-title` attribute to cart table. +* Dev - Template - Product archive anchors are now hooked into templates rather than hard coded. +* Dev - Template - Added template files for the customer details list in emails. emails/email-customer-details.php +* Dev - Template - Revised single variation cart template. Template files now exist for variations, and the cart button will display (disabled) when no selections are made. +* Dev - Template - Made "my orders" columns fully customizable with filters. +* Dev - Template - Unified email template order details tables to use a single template. +* Dev - Allow wc_clean to support arrays. +* Dev - Added a manual update trigger for checkout. +* Dev - Added woocommerce_is_price_filter_active filter to Query class. +* Dev - Replaced some cart methods with dedicated functions. e.g. wc_ship_to_billing_address_only(). +* Localisation - Add Kenyan currency and symbol. + += 2.4.13 - 2016-01-11 = +* Fix - Potential redirect loop when using 'unforce ssl' setting and a https home URL. +* Fix - Escape option names when cleaning up sessions. + += 2.4.12 - 2015-12-9 = +* Fix - 4.4 - Permission error when editing attribute terms. +* Fix - 4.4 - Missing variation images when wp_get_attachment_image_srcset() returns false instead of a string. +* Fix - 4.4 - Use post-thumbnail size in admin to avoid srcset. +* Fix - Webhook status not changed after save with active object-cache. + += 2.4.11 - 2015-12-7 = +* Fix - WordPress 4.4 support. +* Fix - Removes Switzerland from EU VAT definition
. +* Fix - Fix auth endpoint urls. +* Fix - To allow backslash in SKUs. +* Fix - Sanity check for min/max quantity
. +* Fix - 4.4 - Shipping class menu display. +* Fix - 4.4 - Admin menu icons and styling. +* Fix - API - Variable product backorders editing. +* Fix - API - Delete product transients when delete a variable product. +* Fix - API - Returned status when have an invalid oAuth timestamp. +* Fix - API - Early call of order status when editing orders. +* Tweak - 4.4 - Basic support for product embeds. +* Tweak - 4.4 - Support for srcset/sizes and responsive images. +* Tweak - 4.4 - Support for Twenty Sixteen. + += 2.4.10 - 2015-11-10 = +* Fix - Geo IP - Correctly parse .dat files. +* Fix - Geo IP - Ensure WC_Logger class exists before logging errors. +* Fix - Geo IP - Prevent notices in ipv6 methods. +* Tweak - Add information about credit card address for Simplify Commerce. + += 2.4.9 - 2015-11-09 = +* Fix - Check abspath exists in more files to prevent errors on direct access. +* Fix - Hide SQL errors during ajax requests. +* Fix - Fixed redirection loop on customizer screen. +* Fix - Improved error handling in WC_Geo_IP. +* Fix - Bulk edit sale prices. +* Fix - Check for child themes in System Status. +* Fix - API - Warnings when create attributes. +* Fix - System Report: Template version check path. +* Fix - Potential XSS within price.php fixed with escape on get_price() (would require edit/admin permissions to take advantage of). Discovered by FortiGuard Labs (https://www.fortiguard.com/). Template version has been bumped. + += 2.4.8 - 2015-10-26 = +* Fix - Help tips in variations admin. +* Fix - API - Fixed customer count method. +* Fix - Locale switching for city field. +* Fix - Notice in wc_nav_menu_items when endpoint is not set. +* Fix - Loading of correct variation prices when display is true and false in the same page load. +* Fix - Shipping priority for methods with colons in the name. +* Fix - Saving of passwords with '&' inside. +* Fix - Remove double escaping of coupon descriptions. +* Fix - Settings API default value should not apply if value of option is 0
. +* Fix - Avoid potential PHP Fatals by avoiding premature script enqueues. +* Fix - Pass mimes when checking file type
. +* Fix - Reset shipping totals before calculation to prevent totals being used incorrectly. +* Fix - API - Corrected how attributes terms saves non-latin characters. +* Fix - API - Variations price sync. +* Fix - API - Fixed lost variable products data when create/edit an order. +* Tweak - Add trailing slash in get_page_uris to reduce likelihood of conflicts. +* Tweak - API - Added refunded_item_id on GET orders//refunds endpoint. +* Tweak - API - Allow variable products to get retrieved by SKU. +* Tweak - API - Allow edit variations without define the product type to variable. + += 2.4.7 - 2015-09-21 = +* Fix - Handle Switzerland in get_european_union_countries. +* Fix - For geolocation with static cache support, ensure hash is appended during form submission. +* Fix - To prevent discounts being applied in 'random' order (based on order added to cart), sort cart items based on subtotal during calculate_totals. +* Fix - Removed extra ob_start() in class-wc-shortcodes.php. +* Fix - Show counts in category dropdown. +* Fix - Escape add to cart messages to stop translations from breaking cart events. +* Fix - Display of product/order tables in the dashboard when viewed on handheld devices. +* Fix - API order item 'key' value. +* Fix - Check specifically for Post IDs in WC Query verbose rules fix. +* Fix - Only run maybe_set_cart_cookies if cart was loaded to prevent notices. +* Fix - Variation loading/refresh after attribute saving. +* Fix - Added monthly cron schedule. +* Fix - Remove use of 'input' event in checkout scripts to prevent IE11 triggering updates on placeholder change. +* Fix - AJAX variations not being found in some cases when product version was < 2.4, but attributes were updated after sync(). +* Fix - Changed the way variable product prices get cached for greater plugin compatibility. See https://wp.me/p6wtcw-5x +* Fix - Highlighting of reports chart. +* Fix - Network activated plugins not showing up in system status report. +* Fix - Tax fields showing on bulk/quick edit when disabled the tax system. +* Fix - Tax status and tax class values within bulk edit. +* Tweak - Allow bulk edit price to 0
. +* Tweak - Add filters to control "shipped via" text. +* Tweak - Allow line breaks in non-variation attributes. +* Tweak - Renamed wc_var_prices transient to allow them to flush on product save. +* Tweak - woocommerce_save_account_details_required_fields hook. +* Tweak - Only 'count' published variations. +* Tweak - Display of order total in admin with refunds. +* Tweak - Use Geolocation class for customer IP detection. +* Tweak - Use the needs_payment function (DRY). +* Tweak - Tweak wc_create_page to work with trashed pages. +* Tweak - Redirect 'not right now' to referer in onboarding wizard. +* Tweak - woocommerce_update_new_customer_past_order action. +* Tweak - Prevent empty terms when using `wc_get_formatted_variation()`. +* Tweak - Unslash shipping label on orders admin screen. +* Tweak - Prevent wrong phone numbers on PayPal for CA and US when users add the prefix `+1`. +* Template - Removed 'Payment' heading in `templates/checkout/form-pay.php`. +* Template - Removed unnecessary clearing div in `templates/checkout/payment.php`. + += 2.4.6 - 2015-08-24 = +* Fix - menu_order notices on IIS. +* Fix - Grouped product is_purchasable check during add to cart. +* Fix - Subscriptions 2.0 (unreleased) compatibility. +* Fix - Encode variation data in add_to_cart_url method. +* Fix - Bulk update variation: Set manage stock when _manage_stock meta data is missing. +* Fix - Bulk update variation: Allow stock to be set to 0. +* Fix - Ajax variation < 2.4 attribute name handling. +* Fix - During updates, only recreate .htaccess if not using redirect download method. +* Fix - Handle non standard decimals in flat rate costs. +* Tweak - WC Setup wizard: Fix manual setting of decimal/thousand separator. +* Tweak - Set ajax/nocache headers for ajax requests. +* Tweak - Add tooltips for tax status and tax class options. +* Tweak - WC Setup wizard: multi-line step styling. +* Tweak - WC Setup wizard: site icon display on WP 4.3. +* Tweak - WC Setup wizard: tweaked wording. +* Tweak - WC Setup wizard: Add spinner/loading indication between onboarding steps. +* Tweak - Allow HTML in store notice. + += 2.4.5 - 2015-08-20 = +* Fix - Global text based attribute saving on product page. +* Fix - save_account_details should check display name of current user. +* Fix - Show the right 'no shipping available' message when a country does not have states. +* Fix - Add required postcode marker after label replace. +* Fix - Flush product cache so prices are regenerated after scheduled sale ends. +* Fix - Removed /page/ when using layered nav dropdown. +* Tweak - Allowed Zip/Post Codes description for Local Delivery. +* Tweak - Improve display_item_downloads numbering and use same function in emails. +* Tweak - API - Fixes notices about deprecated `$HTTP_RAW_POST_DATA` on PHP 5.6. +* Tweak - In add_to_cart_action, check is_purchasable rather than post status. +* Tweak - Add expand/close links for attributes and match variation UI. +* Tweak - Added locale info for BD, NP, JP and HU +* Tweak - woocommerce_delete_version_transients_limit filter. +* Tweak - Suppress errors when calling set_time_limit to avoid hosting conflicts. +* Tweak - Keep new variation in sync so actions can modify data. +* Tweak - Improved download numbering in emails and order page. +* Tweak - Allowed users to install translations for the current language during the Setup Wizard. + += 2.4.4 - 2015-08-14 = +* Fix - Ajax variation handling when 'any' attribute is set. +* Fix - Run html_entity_decode over text attributes to fix problems with quote characters. +* Fix - COD: remove shipping check if the cart is 100% virtual. +* Tweak - Order variations by menu_order by fallback to ID. +* Tweak - Include attribute archives support in the breadcrumbs. +* Tweak - woocommerce_variable_children_args hook. + += 2.4.3 - 2015-08-12 = +* Fix - Query within wc_customer_bought_product(). +* Fix - Tab hiding with some theme markup. +* Fix - Ajax variations: stripslashes to fix attributes with quotes. +* Fix - No longer returns to the first variation list page when deleting one variation. +* Fix - Refund subjects when order contains downloadable product. +* Fix - wc_get_product_variation_attributes should only get parent attributes which are for variations. +* Tweak - Disable display_errors during ajax requests to prevent malformed JSON. +* Tweak - When merging shipping taxes with a shipping rate taxes, ensure shipping rate taxes is not malformed. +* Tweak - Improved refund email events and woocommerce_order_fully_refunded hook. + += 2.4.2 - 2015-08-11 = +* Fix - If all variations are out of stock, maintain pricing display. +* Fix - Prevent double add to cart due to ajax endpoints. +* Fix - ordering_args in product_category shortcode. +* Fix - Tax inclusive prices rounding case. +* Tweak - If no variation prices are found, show no price label rather than free. +* Tweak - Made tab panel selector more specific to avoid theme conflicts. +* Tweak - Made checkout make use of new ajax endpoints. +* Tweak - woocommerce_force_ssl_checkout no longer needs to check for admin-ajax actions. +* Tweak - Hide get_formatted_legacy notices when doing ajax. +* Tweak - use shop_single instead of full image size for variations. + += 2.4.1 - 2015-08-10 = +* Fix - Tweaked the 2.4 upgrade routine to disable refund emails during update. +* Fix - Notices when calling get_shipping_classes(). +* Fix - Added upgrade routine to ensure _stock_status meta exists for variations created before WooCommerce 2.2 + += 2.4.0 - 2015-08-10 = +* Feature - Onboarding/setup wizard for new users to handle basic store settings and installation. +* Feature - Improved help tabs with inline video tutorials where applicable. +* Feature - New AJAX powered variations interface to improve edit product page loading times and posting large amounts of data. +* Feature - For products with many variations, on the frontend switch to AJAX to load matching variations based on user input attributes, instead of doing it all inline. +* Feature - Show full category hierarchy in permalinks. +* Feature - Added priorities for shipping methods to give more control over defaults. +* Feature - [Added a new geolocation option to support static page caching using AJAX and a querystring.](https://woocommerce.wordpress.com/2015/07/02/making-geolocation-static-cache-friendly-in-2-4/) +* Feature - Email notifications for partial refunds. +* Feature - Visual API authentication endpoint for 3rd party use. +* Feature - API key generation changes. Secret keys no longer stored in database. +* Feature - [Refactored Flat Rate Shipping for simplicity.](https://woocommerce.wordpress.com/2015/06/simplifying-flat-rate-shipping-in-wc-2-4/) +* Feature - Made international shipping UI the same as flat rate. +* Feature - New ajax endpoints to improve performance by avoiding admin overhead. +* Fix - Ensure coupon taxes are reset when calculating totals. +* Fix - Improve discount amount rounding. +* Fix - Update order shipping after editing shipping from API. +* Tweak - Moved country next to other address fields in Checkout UI. +* Tweak - Improved reports, in particular for refunds. +* Tweak - Improve save_attributes ajax function to correctly save text attributes. +* Tweak - Base discounts on the undiscounted price. #5874 +* Tweak - Added wc_product_cat_class functions. +* Tweak - Display related products and upsells in 4 columns. +* Tweak - Only redirect to welcome page for MAJOR versions/updates. +* Tweak - GeoLocation IPv6 database. +* Tweak - Improved text based attribute handling to prevent issues with slashes. +* Tweak - Ajaxified the grouped product option. +* Tweak - Email template improvements and wider email client compatibility. +* Dev - Created a template file for the Proceed to Checkout button. +* Dev - API version v3. +* Dev - API - Implemented full support for Basic Authentication for v3, following the RFC 2617 specs. +* Dev - API - Fixed Oauth 1.0a to strictly follow all specs from RFC 5849 for v3. +* Dev - API - Added an endpoint to handle product attributes. +* Dev - API - Auto generete passwords for new customers only when enabled the generate_password option. +* Dev - API - Added display and image on product categories response. +* Dev - API - Added endpoint for bulk update/insert coupons, customers, products and orders. +* Dev - API - Deprecated /product/sku endpoint from v3. +* Dev - API - Created the /products/id/orders endpoint to fetch orders containing a specific product. +* Localisation - Added Argentine currency and provinces. +* [Various other small fixes and enhancements.](https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+milestone%3A%222.4+Helpful+Hedgehog%22+is%3Aclosed) + += 2.3.13 - 2015-07-07 = +* Fix - Improved the email settings save and tabs for 3rd party plugins. +* Fix - Datepicker range for variations. + += 2.3.12 - 2015-07-06 = +* Fix - Fixed Google Chrome forcing to use SSL. This can cause some issues on websites behind load balancers or reverse proxies. [Read more](https://docs.woocommerce.com/document/ssl-and-https/#websites-behind-load-balancers-or-reverse-proxies). +* Fix - Escaped shop url in empty cart template. +* Fix - Escaped product tabs titles. +* Fix - Removed deprecated PHP4 constructor on Widget classes. +* Fix - Wrong `price_slider_updated` JS event arguments. +* Fix - Stock quantity type in WC-API. +* Fix - Don't reveal username when login failed on valid email login. +* Fix - Fatal error on order details when have some downloadable product deleted. +* Fix - Relative paths validation for downloadable product. +* Fix - Flat rate shipping costs should ignore virtual items. +* Tweak - Keep product quantity when happens some error while add product on the cart. + += 2.3.11 - 2015-06-10 = +* Fix - Check if rating is enabled before check if rating is required to a review. +* Fix - get_discounted_price needs to check if taxes are enabled. +* Fix - Fixed filetype check for digital downloads. +* Fix - Newfoundland and Labrador state rename. +* Fix - Escaped js in widget layered nav when use the dropdown option. +* Fix - Switch the permissions check for json_search_products to use the read_product capability. +* Fix - Fixed the addition of variable products using the Order API. +* Fix - Sale item exclusion logic for variations. +* Fix - Clear correct variation stock transients when setting stock. +* Fix - Switch to JSON to avoid unserializing untrusted data when handling responses from PayPal. +* Fix - API - Fixed the sanitization for downloadable files on products endpoint. +* Tweak - woocommerce_downloadable_file_exists filter. + += 2.3.10 - 2015-06-01 = +* Fix - Fixed theme check notice for core supported themes. +* Fix - Add RTL direction to emails. +* Fix - Fixed product category media upload modal. +* Fix - Coupon maximum discount calculation. +* Fix - PayPal icons and URLs. +* Fix - API - Fixed subtotal_tax round and decimal dp. +* Fix - Wrap payment js in jquery. +* Fix - Delete correct transient when linking variations. +* Fix - Set default currency position format string (in case of missing or invalid `woocommerce_currency_pos` option value). +* Fix - Simplify Commerce undefined constant ('error_code' > '$error' typo). +* Fix - Fixes too many arguments in function or method call: WC_Shortcode_My_Account::add_payment_method. +* Fix - Pass correct number of arguments to `wc_lostpassword_url()`, `wc_nav_menu_items()`, `wc_nav_menu_item_classes()`, and `wc_change_term_counts()`. +* Fix - Fixes usage of void return value from `wc_cart_totals_taxes_total_html()`. +* Fix - Missing global in `render_product_columns()`. +* Fix - Add `$args` arguments to `WC_Product_Factory->get_product_class()` to allow `$product_type` to be overwritten by `$args['product_type']`. +* Fix - Remove call to `wp_specialchars_decode()` in `wc_get_price_thousand_separator()` and `wc_get_price_decimal_separator()`. +* Fix - fclose in logging class requires a resource, not a string. +* Fix - Prevent (admin) SQLi when setting stock levels for product variations. +* Tweak - Extra escaping of customer emails in `wc_customer_bought_product()`. +* Tweak - Improve tooltip sanitization. +* Tweak - Escape provided array of post codes in tax class. +* Tweak - Escape metadata when duplicating products. +* Tweak - Escape permalink settings slugs. +* Tweak - Sanitize columns value in shortcodes. +* Tweak - Use prepare for updating attributes. +* Tweak - Use wp_safe_remote_ functions in place of wp_remote_ where applicable. +* Tweak - Added extra capability checks to notices, email template editing, and admin ajax requests. +* Tweak - Set nonce_user_logged_out to WC session ID, if set. +* Tweak - Added `wc_send_frame_options_header` function to prevent checkout and account pages from being used in iFrames. Added via filter so this can be disabled. +* Tweak - Validate file types are allowed for downloadable products when saving. +* Tweak - Filter: woocommerce_cart_item_removed_title +* Tweak - Update html-admin-page-status-report.php to show unaltered URLs. +* Tweak - When updating transients, clear previous version of transients. +* Tweak - Replace max_related_posts_query for performance reasons. +* Tweak - Combine transients for get_rating_count. +* Tweak - Bump the PrettyPhoto version during enqueue to flush caches. +* Tweak - Remove all instances of sslverify=false #8058 +* Tweak - Error prevention when showing customer orders on the frontend. +* Tweak - Added PH states. + += 2.3.9 - 2015-05-19 = +* Fix - Fixed language upgrader verification. +* Fix - Refund reporting #8010 +* Fix - Redirect after bulk editing. +* Fix - Prevent variable overwrite in save_product_meta. +* Fix - Fix stock report pagination. +* Fix - Fixed paypal about URL for Malta. +* Fix - Fixed save downloadable files for product variations. +* Fix - Remove submitdiv and fix post status updating. +* Fix - Fixed the sort order dropdown items when create new product attributes. +* Fix - Move action to prevent infinite recursion on login/restoring saved carts. +* Fix - Update PrettyPhoto to 3.1.6 to resolve XSS security issue https://github.com/scaron/prettyphoto/issues/149 + += 2.3.8 - 2015-04-20 = +* Fix - Ensure coupon taxes are reset when calculating totals. +* Fix - Downloads url sanitization to work correctly with shortcodes and urls. +* Fix - State/Country select2 issues with Internet Explorer. +* Fix - Flat rate per item and per class if no additional costs added. +* Fix - Simplify Commerce compatibility with free trial subscriptions. +* Fix - Select2 z-index in the admin. +* Fix - Postmeta records deletation on plugin uninstall. +* Fix - List only approved comments in products reviews on API. +* Fix - Improved variation SKU display. +* Fix - Prices including tax within orders. +* Fix - Ensure line taxes are stored when calculating tax theough the API. +* Fix - Add null date check for download permissions. +* Fix - PayPal Logging. +* Fix - Coupon product id and category id checks should run for all coupon types. +* Tweak - Hook in cart totals via action. +* Tweak - Prevent errors when adding or deleting products for the coupon. +* Tweak - Prevent errors when check customer capability to view orders. +* Tweak - Ensure Price Filter links has a trailing slash to avoid pagination issues. +* Tweak - Improved the check for mismatched totals in items lines for PayPal payment gateway. +* Tweak - Use wc_stock_amount to format API orders stock amount. +* Tweak - Ensure show_in_admin_all_list is respected for order statuses. +* Tweak - Remove rounding from shipping costs for greater shipping tax precision. +* Tweak - Only automatically cancel orders created via checkout + allow post_parent to be set. +* Tweak - Deny all access to revisions through API. + += 2.3.7 - 2015-03-18 = +* Fix - Allow saving of empty download expiry date on orders. +* Fix - get_total_discount() function with certain tax setups. +* Fix - stock management for variations for Products API. +* Fix - Price filter styling. +* Fix - Support price filter min or max only. +* Fix - Allowed paths for file url. +* Tweak - Chile address format. +* Tweak - Revised how discounts/discount taxes are stored for consistency. Always store ex. tax to make data retrieval easier, and to ensure totals are correct after settings changes. Backwards compatibility maintained through use of order versioning. +* Tweak - Delete product attachments when the Products API fails. + += 2.3.6 - 2015-03-13 = +* Fix - Removal of coupons containing spaces. +* Fix - Unclosed div in profile page. +* Fix - Export report CSV. +* Fix - Settings API - allow multiselect fields to be emptied. +* Fix - Saving an order needs to save the discount amount ex. tax like the cart. +* Fix - Order again with custom attributes. +* Fix - [CVE-2015-2329] Prevent potential XSS within tooltips (discovered by Fortinet FortiGuard Labs). +* Fix - Paypal debug option. +* Fix - Removed $q->query['wc_query'] = 'product_query' which broke redirects (#7703). Use $q->get('wc_query') instead. +* Fix - Sanitize tax_rate_id when saving taxes in the backend to prevent potential SQL injection (discovered by WordFence). +* Tweak - Show discounts inc. tax when showing order totals inc. tax. +* Tweak - Use 30 days instead of year for transients to avoid bugs in memcache plugins. +* Tweak - Add reports menu item if user can access reports but not the main WC section. +* Tweak - Improve grouped product quantity inputs. +* Tweak - Load the persistent cart if cart is empty. +* Tweak - Prevent cart being cleared when accessing the login page. +* Tweak - Shipping calculator - Made state/postcode respect country locale like checkout. +* Tweak - Move default customer location to general settings tab. +* Tweak - Only run save_category_fields for product_cat taxonomy. +* Tweak - Improved message when variation attributes are missing. +* Tweak - Allow wc_attribute_label to support product-level attribute names. +* Tweak - Added the option to not round the line total. +* Tweak - Improved coupon percent calculation for fixed discounts. +* Tweak - Show calculate total when shipping is needed, but shipping is hidden. +* Tweak - Cart total labels. +* Tweak - Increase wc_get_weight precision. +* Dev - API - reports/sales now also returns total refunds. + += 2.3.5 - 2015-02-20 = +* Fix - Plain text address formatting. +* Fix - Detect shortcodes when saving URLs. +* Fix - Unhook wc_page_endpoint_title after it is ran once (main page title). +* Fix - Taxes save issue when page is paginated. +* Fix - Cross/up-sells should not search variations. +* Fix - Related post offset. +* Tweak - Round report values. +* Tweak - Text in plain text emails. +* Tweak - Improve category coupon message. +* Tweak - Don't download GeoIP Database until geolocation option is enabled in settings. + += 2.3.4 - 2015-02-17 = +* Fix - limit_usage_to_x_items option in coupons. +* Fix - Run coupon codes through html_entity_decode. +* Fix - Tax by code report for refunds. +* Fix - Auto-generation of slug when adding new attribute. +* Fix - Prevented errors when `DOMDocument` is not found (used for your HTML/Multipart emails). +* Fix - Load WC css on user edit screen. +* Fix - DB error when showing reports by product without selecting a product. +* Fix - Stock status when updating out of stock product. +* Fix - Fix place order button text on init. +* Fix - When duplicating products, handle entities. +* Fix - Double shop page in breadcrumb and white space issues. +* Fix - When purchasing multiple downloadable products (same item), multiply download limit by qty purchased. +* Fix - Added checks for gzopen to prevent activation errors. +* Tweak - Added DOMDocument item in the System Status as a requirement. +* Tweak - Simplify default mode should be 'standard'. +* Tweak - Set attribte 'query_var' true when public. +* Tweak - Use wc_get_page_permalink() to get page permalinks. +* Tweak - Register shop_order post statuses earlier to ensure statuses are registered for cron. +* Tweak - Improvements to refund handling in Taxes by code/date, and sales by date reports. Gross/net excludes refunds. +* Tweak - Share data between Sales by Date report and API. +* Tweak - Related posts - replace ORDER BY RAND() with random offset. +* Tweak - Run item meta label through wc_attribute_label() in admin order page. +* Tweak - Run File URLs through esc_url_raw instead of wc_clean to preserve spaces. +* Tweak - Small timeout on checkout update action to prevent several triggering at once. +* Tweak - Restock items AFTER refund, not before. +* Tweak - If logged in, populate customer data from user meta. + += 2.3.3 - 2015-02-12 = +* Fix - Potential notice with preg_match wildcard search, if used incorrectly. +* Fix - Typo in get_from_name method. +* Fix - Fix errors during checkout when mb_convert_encoding() is not supported. +* Fix - Change hooks used to output post columns - fixes columns after quick edit. +* Fix - Only apply product/cat coupon checks for cart to cart coupons. +* Fix - Query in uninstall script. +* Tweak - Only run the uninstaller if the "Uninstall on Delete" option is checked in system status. + += 2.3.2 - 2015-02-12 = +* Fix - Item meta removal query in order class. +* Fix - Pass correct shipping cost to PayPal. +* Fix - Flat rate extra costs when costs are an array. +* Fix - When ratings are required for reviews, ensure validation is performed if the rating element is removed from DOM. +* Fix - When updating shipping in cart, keep shipping calculator in DOM. +* Fix - WC_TEMPLATE_DEBUG_MODE in admin. +* Fix - Average product rating when ratings are not required. +* Fix - attribute_public option. + += 2.3.1 - 2015-02-11 = +* Fix - When the geolocation database cannot download, ensure the correct method is used to log the error. +* Fix - Notice in woocommerce_form_field(). +* Fix - attribute_public notice before DB upgrade. +* Fix - [products] ids and sku args. +* Fix - Backwards compatibility for (deprecated) $tax variable in WC_Cart. +* Fix - is_available() check in local pickup. +* Fix - Added WC version of GEOIP classes to prevent conflicts with other plugins. + += 2.3.0 - 2015-02-11 = +* Feature - Option to geo-locate the customer's initial location. +* Feature - Display taxes in store based on the customer location, rather than the shop base. +* Feature - Made tax importer expand postcode ranges. +* Feature - Print styles for reports. +* Feature - Remove products from the cart in the widget. +* Feature - Bulk edit sales schedule on variations. +* Feature - Fresh new frontend / email design. +* Feature - Undo link in message when removing products from the cart. +* Feature - Compatibility with Twenty Fifteen default theme. +* Feature - Added 'top freebies' to product report. +* Feature - Added numeric sort for attributes. +* Feature - Added support for some Jetpack features: Omnisearch, Publicize and Markdown editor. +* Feature - UI for adding Webhooks. +* Feature - Show Gross and Net totals in reports. +* Refactor - Removed deprecated methods from WC_Frontend_Scripts and rewrote script registration and localization to run once. +* Refactor - Routing all email functionality through one send() method. +* Refactor - Replaced existing email css inliner with Emogrifier. +* Refactor - get_product_search_form(). +* Refactor - Improved the Shipping Class field in products quick edit and bulk edit. +* Refactor - Removed style settings in favour of separate plugin. +* Refactor - Removed quantity increment/decrement buttons in favour of separate plugin. +* Feature - Added link on purchased products list on orders screen. +* Fix - When 'hide out of stock products' is disabled, out of stock variations / attributes are now visible. +* Fix - Fix cart coupon on-sale checks for variations. +* Tweak - Double the default product image dimensions. +* Tweak - Added refunds to Sales by Date report. +* Tweak - Updated prevent_caching() method to work if a cart/checkout page isn't set. +* Tweak - When user tries to download a file and isn't logged in, send them to the account page with a notice. +* Tweak - Logic in wc_paying_customer to only increase for 'simple' orders. +* Tweak - Added tool to refresh stats to customer list. +* Tweak - Recent order table on my account is responsive. +* Tweak - Drop WC tables in wpmu_drop_tables (for multisite). +* Tweak - Moved 'Proceed to checkout' button on cart to beneath totals. +* Tweak - Improved 'responsiveness' of product data tabs on add/edit product screen. +* Tweak - Added 'stupidtable' script to allow order item sorting on the order screen (by name, cost, qty). +* Tweak - In the cart, add variation selected data to the permalink. +* Dev - API - Look up product by sku. +* Dev - API - New parent_id param for products API. +* Dev - API - Sales data in API now matches sales data in WooCommerce reports page. +* Dev - API - Added 'net_sales' data to reports. +* Dev - API - catalog_visibility is set to visible by default in products API. +* Dev - API - Added new filter to query for specific products by ID: /products?filter[in] +* Dev - Made template debug mode set WC_TEMPLATE_DEBUG_MODE constant and remove all overrides for all template loading functions. +* Dev - Switched to .scss from .less for all styles. +* Dev - Included bourbon for scss mixins. +* Dev - Decoupled the order summary and payments area. Both are updated independently via ajax fragments and can be moved around via actions. TEMPLATES OVERRIDING THESE TEMPLATES WILL NEED TO UPDATE THEIR FILES. +* Dev - Moved WC_Cart::get_cart_from_session() and dependencies to a later hook (was init, now wp_loaded). +* Dev - Migrated away from CHOSEN to SELECT2. Chosen is still registered in case 3rd parties try to enqueue. +* Localisation - Add Ukrainian currency and symbol. +* Localisation - Greece regions. + += 2.2.11 - 2015-01-29 = +* Add - URL in Usage/Limit column in Coupons table to query for orders. +* Fix - esc_url() applied to prevent potential XSS issues. +* Fix - "Link all variations" button. + += 2.2.10 - 2014-12-16 = +* Fix - Stock status on quick and bulk edit. +* Fix - Incorrect clearing of error messages. + += 2.2.9 - 2014-12-15 = +* Add - API - parent_id for products endpoint. +* Fix - Processing and On-hold order links in WooCommerce Status dashboard widget. +* Fix - Orders API when query orders with deleted products. +* Fix - Check order exists in wc_clear_cart_after_payment(). +* Fix - move $cart_updated inside $passed_validation to prevent unnessary updates. +* Fix - MX states keys. +* Fix - sanitize_user correctly during registration. +* Fix - API - Variation handling for stock data. +* Fix - When bulk editing variable products, set the stock status for non-stock managed variations. +* Fix - Fix coupons by date queries to prevent inflated results. +* Fix - During refunds, correctly set shipping tax totals. +* Fix - Ensure floats are safely converted to strings. +* Fix - remove_taxes needs to clear line_tax_data. +* Fix - Correctly save custom address fields in admin. +* Fix - API - Fixed a bug for save multiple images from the media library in products endpoint. +* Fix - API - Delete products when happens some error. +* Fix - API - `enable_free_shipping`, `product_category_ids`, `exclude_product_category_ids` and `customer_emails` coupons params. +* Fix - API - Coupons `expiry_date` format. +* Fix - Force HTTP option behavior on Customizer Preview screen. +* Fix - Cart error messages when the session is expired. +* Tweak - API - set_fee should support tax_data. +* Tweak - Don't force tax_rate_id to an integer. Allow strings. +* Tweak - Additional filters inside tax class to support extensions. +* Tweak - Allow plugins to filter the taxable location. +* Tweak - Added result and message keys to order_review AJAX call. +* Tweak - Added get_cart_item to WC_Cart class. + += 2.2.8 - 2014-10-29 = +* Fix - Image crop option. +* Fix - Display of order note date. +* Fix - API POST/PUT products attributes values. +* Fix - Added fallbacks to wp_get_referer(). +* Fix - PayPal encoding for return urls. +* Fix - Low stock report should hide no stock. +* Fix - Fixed nonce check in form handler. +* Fix - Notices in status report when checking if templates exist. +* Fix - Allow to filter empty tax rate code. +* Fix - Fixed the value format in stock field with wc_stock_amount(). +* Fix - Remove strtolower for status names and capitalize statuses. +* Tweak - Removed unused methods from PayPal gateway. +* Tweak - Use current user ID for refunds. +* Tweak - Allow API edit_product method to update post_name (slug). + += 2.2.7 - 2014-10-22 = +* Fix - Fix refund date. +* Fix - Fixed various notices. +* Fix - Make updater set parent backorder status. +* Fix - In the US address format, use state code rather than the full state name. +* Fix - Use mb_strtolower to prevent issues with unicode chars. +* Fix - Introduced the wc_strtolower() function +* Fix - Make cart total consider taxes when saving an order. +* Fix - Fix /shop/ base URL Non Latin issue with url decode. +* Fix - Correct report handling for full and partial refunds. +* Fix - Update jquery payment to prevent autocomplete issues. +* Fix - Coupon API: Don't return current timestamp when expiry_date is not set. +* Fix - wc_update_product_stock should update stock regardless, if the meta data doesn't currently exist. +* Fix - Added wp_kses_post to purchase note +* Fix - Fixed edit account page fields #6577. +* Fix - Fix stock report queries #6565. +* Fix - Fix error message with maximum amount in coupon class. +* Fix - Fix nonce usage during checkout/account pages. +* Fix - Incorrect conversion of Unicode characters in order status names. +* Fix - Edit Account fields order. +* Fix - Shipping address values on checkout page. +* Fix - Enforce slug format of translated edit-address-slugs. +* Tweak - Allow for non-integer stock quantities. +* Tweak - Update simplify commerce to use new $order->get_status(). +* Tweak - Only show integrations subnav when there are multiple integrations. + += 2.2.6 - 2014-10-08 = +* Fix - Notices in the cache helper. +* Fix - Prevent bulk edit from breaking sale price scheduled dates. +* Fix - Prevent address fields being empty when editing an address within an order. +* Fix - Removed save_post remove_action call which breaks 3rd party plugins. See ticket #6376 and #6485 for details. +* Fix - Prevent warnings when set "Specific Countries" empty in shipping methods. +* Tweak - Added woocommerce_product_subcategories_hide_empty filter. +* Tweak - Added filter for shipping tax. +* Tweak - Product attribute shortcode should return columns css class. + += 2.2.5 - 2014-10-07 = +* Fix - Filters in admin screen for coupons and orders. +* Fix - When bulk editing, don't allow sale price to be negative. +* Fix - When manually adding items to an order, show tax columns. +* Fix - When manually adding items to an order, include variation data. +* Fix - Prevent errors when constructing WC_Order without an ID. +* Fix - Item_id notices in email templates. +* Fix - Use variation get_stock_quantity() for variation max_qty. +* Fix - Prevent bulk edit cancel from clearing options when bulk editing variations. +* Fix - Use term_taxonomy_id for transient names - fixes counts in layered nav. +* Fix - Use wc_get_order in simplify-commerce. +* Fix - Use 'no' instead of boolean to disable PayPal gateway. +* Fix - Do not escape redirect url in form handler - fixes malformed URLs. +* Fix - Prevented non-existant pages from breaking cache helper. +* Fix - Prevent sale prices showing errors in admin wrongly. +* Fix - Prevent order statuses affecting other queries. +* Fix - Removed deprecated get_page() functions. +* Fix - Category archives. WP core still has issues dealing with pad_counts + parent when getting categories. Workaround by not hiding empty cats, then filtering the returned list using wp_list_filter. +* Fix - When formatting meta data for display, suffix items to prevent issues when there are multiple values for the same meta key. +* Fix - Unhook save_meta_boxes after first successful run to prevent race conditions. +* Tweak - Added refunds to Sales by Date report. +* Tweak - Tweak load_plugin_textdomain to be relative - this falls back to WP_LANG_DIR automatically. Can prevent "open_basedir restriction in effect". +* Tweak - Added acceptance marks to PayPal Standard where applicable to replace generic PayPal icon. + += 2.2.4 - 2014-09-18 = +* Fix - Prevent errors when adding 'zero-rated' tax on checkout. +* Fix - Fixed a varation product width inheritance bug. +* Fix - Totals in taxes by date report. +* Fix - Fix the 'only 1 visible product' redirect to not trigger when paging results. +* Tweak - Improved headers sent notice to include file and line. +* Tweak - When updating order status, ensure its a valid WC order status. +* Tweak - Add notice when order is no longer editable. +* Dev - Allow getting rating count for a specific rating value #6284. +* Localisation - Nepal States. +* Localisation - Mexico states. + += 2.2.3 - 2014-09-16 = +* Fix - Order status translation in admin and account page. +* Fix - Ensure shipping address gets displayed - fixes needs_shipping_address() method. +* Fix - Escaping of country names in tax settings. +* Fix - Encoding of pagination link when using default permalinks. +* Fix - NPR currency. +* Fix - Fixing "Invalid key" error when clicking link in password reset email. +* Fix - Mobile checkout via PayPal when using tax inclusive prices. +* Fix - Thumbnails "hard crop" option. +* Fix - Missing variables when add new product variation. +* Fix - Fixed minor XSS issue on reports screens by escaping and sanitizing 'range' GET variable. +* Fix - Number format when calculate the line items tax. +* Fix - Language update/install in Multisites. +* Fix - "Set product image" media gallery title in non-product post type. +* Fix - Number of processing orders in WooCommerce > Orders menu. +* Fix - Issue that preventing cookies being set on shutdown after wp_send_json. +* Fix - Incorrect shipping calculation because of missing width in product variation. +* Tweak - Display of locale information on system status page. +* Tweak - Removed postcode for Bahamas. +* Tweak - In system status, show path to template file override. +* Tweak - Dynamically get the address fields in WC_Checkout::create_order() +* Tweak - If a refund fails, delete refund post. +* Tweak - Button for hide the language update message. +* Tweak - Method for install the translations directly. +* Tweak - Display of h4 in settings pages. +* Dev - Added woocommerce_get_settings_ID filters. + += 2.2.2 - 2014-09-11 = +* Fix - Saving of variation stock when parent stock management is disabled. +* Fix - "open_basedir restriction in effect" error caused on install when trying to create the WC logging directory. +* Fix - For regular products, ensure stock level saves on product creation. + += 2.2.1 - 2014-09-10 = +* Fix - Small tweak to the installer to prevent errors caused by outdated plugins. +* Fix - Mijireh Checkout update link. +* Tweak - Small tweak to update notification to remind users to update old plugins prior to install. + += 2.2.0 - 2014-09-10 = +* Feature - Refunds system for orders. +* Feature - New orders panel for managing line items + totals. +* Feature - Language pack downloader. po and mo files removed from core (too heavy). +* Feature - Added used payment gateway to view orders screens. +* Feature - Allow backorders to be configured at variation level. +* Feature - Protect admins from shop manager users. +* Feature - Ability to add custom quantity using add_to_cart shortcode. +* Feature - Ability to set a maximum spend for coupons. +* Feature - Added Simplify Commerce payment gateway. +* Fix - Allow endpoint use on the front page. +* Fix - user_activation_key password reset code. +* Tweak - Recalculate the cart totals, in the event a user registers during checkout and in doing so qualifies for any discounts. +* Tweak - Use `woocommerce_valid_order_statuses_for_payment` in `pay_action` too. +* Tweak - Added the possibility to translate the edit-address endpoint slug. +* Tweak - Removed all the_content filter in favor to wpautop() and do_shortcode(). +* Tweak - Send IPN email notifications to new order email. +* Tweak - Clear and wipe session data on logout and end of checkout for guests. +* Tweak - Load archive-product.php for other product taxonomies. +* Tweak - Disable image size settings if filters are being used. +* Tweak - Hide the shipping address when local pickup is used. +* Tweak - Password protected posts are not hidden from catalog by default anymore, visibility can be set via the 'Catalog visibility' option. +* Tweak - Removed the shortcode button in favor to [WooCommerce Shortcodes](https://wordpress.org/plugins/woocommerce-shortcodes/) +* Dev - API Version 2 with push support. +* Dev - API: Lookup customers by email endpoint. +* Dev - API: Allow ordering on the resource level. +* Dev - Customers API / Methods PUT/POST/DELETE. +* Dev - Coupons API / Methods PUT/POST/DELETE. +* Dev - Orders API / Methods PUT/POST/DELETE. +* Dev - Products API / Methods PUT/POST/DELETE. +* Dev - Added description parameter to the woocommerce_form_field function. +* Dev - Introduce `woocommerce_valid_order_statuses_for_payment_complete` filter. +* Dev - Introduce `woocommerce_thankyou_order_received_text` filter. +* Dev - Introduce `woocommerce_product_backorders_allowed` filter. +* Dev - get_user and get_user_id methods. +* Dev - Add new 'wc_admin_reports_path' filter to reports. +* Dev - Add user ID to shipping packages. +* Dev - Added product id parameter to related posts filters. +* Dev - WC_LOG_DIR constant for defining the log directory. +* Dev - Moved default logging directory 1 level above WordPress, rather than in the plugin folder. +* Dev - Added log viewer in System Status. +* Dev - Made stateless classes static to allow unhooking of methods. +* Dev - Introduces the wc_get_log_file_path() function. +* Dev - Introduces the WC_Order::needs_shipping_address() method. +* Dev - Gateways can set transaction ID for the order. +* Dev - Gateways can do refunds via the Payment Gateway API. +* Refactor - Changed the method in which order statuses are stored. Previously, order status was a taxonomy. This caused issues when unique term slugs differed from what we were expecting, and also added additional overhead to order queries in reports. https://github.com/woocommerce/woocommerce/issues/3064 Order status is now stored as post status - several new post statuses have been added. Order class variables are backwards compatible. The only thing to note (for devs) is that any query must use the order status instead of 'publish' when getting orders and querying by post_status. THe shop_order_status has also been removed. +* Refactor - Update stock amounts with DB queries. +* Refactor - Simplified attribute name sanitisation which maintains UTF8 char integrity. +* Refactor - Country class return methods. +* Notice - Deprecated Mijireh gateway in core. Plugin is available on .org. +* Localisation - Egypptian currency. +* Localisation - Address format of Taiwan. +* Localisation - Removed language files from core to made the package lighter (see language pack downloader feature). + += 2.1.12 - 2014-07-01 = +* Fix - Total tax should be +, not -. +* Fix - Address format in plain text emails to use line breaks, not commas. +* Fix - order item count fix and tr class filters. +* Fix - Missing translations during checkout. +* Fix - Correctly clear transients, including sale transient. +* Tweak - woocommerce_get_order_item_totals_excl_free_fees hook. + += 2.1.11 - 2014-06-09 = +* Fix - Plain text email display of customer address. +* Fix - Saving tax rates threw notices (missing git cherry pick). + += 2.1.10 - 2014-06-03 = +* Fix - Removed unnecessary localization from edit account. +* Fix - Admin welcome screen css. +* Fix - Fixed my account setting values to wrong user submitted strings. +* Fix - Menu order terms were coming back empty. +* Fix - Fix notice that occurs from external function call. +* Fix - Addons page, reference new json API endpoint. +* Fix - Notices when rendering WooCommerce Shop as Front Page. +* Fix - Prevent undefined notice for Layered Nav title. +* Fix - state_province is not required for mijireh any longer. +* Fix - Fix coupon limit checks and enhance to check ID by provided email (if logged out). +* Fix - Danish krone symbol. +* Fix - check for the existence of the cart during the is_available(). +* Fix - Fixes performance degradation on large wp_options tables. +* Fix - improved the shortcodes button for support WordPress 3.9. +* Tweak - Stronger session ID generation. +* Dev - Add action hooks when saving tax rates. + += 2.1.9 - 2014-05-14 = +* Fix - fix case-insensitive matching for coupon posts with uppercase chars. +* Fix - Make the welcome page RTL compatible. +* Fix - Sanitize, but decode, flat rate shipping method ids. UTF-8 Friendly. +* Fix - Stop sending line items to Mijireh. Like PayPal, Mijireh struggles with out prices including tax due to rounding errors. Since the validation cannot be disabled, its better to just send the order as 1 item. This will prevent rounding errors and payment failures. Prices excluding tax are unaffected. +* Fix - Fix fee/coupon lines typo in REST API order response. +* Fix - Fixes a fatal error when WC()->payment_gateways()->get_available_payment_gateways() is called in the admin. +* Fix - is_available check in shipping for excluding countries was backwards. +* Fix - Encoding of @ in download links. +* Fix - Revise how variation attributes are deleted/updated. Prevents issues with WPE caching when you delete and then update right after. +* Fix - Trim commas and empty lines off address formats. +* Fix - defined a min value to cart quantity input. +* Fix - Fix qty input styling in Firefox 29. +* Fix - Use WP SEO class method rather than deprecated fn. +* Fix - Cleaned up logic in email_instructions. +* Fix - Prevent empty session data being stored until a cookie or session exists to retrieve it. +* Fix - fixed WC_Product_Variable::set_stock() compatibility with WC_Product::set_stock(). +* Fix - Fix notice when not scanning any files in system status. +* Fix - Made wc_get_product_terms support custom menu_order by using get_terms and an include. +* Fix - Correct character 3 vaildation for UK postcodes. +* Tweak - Add a tip for default selections, and use opt groups for the long bulk edit list. +* Tweak - Option to toggle enable_for_virtual for COD, rather than just doing it. +* Dev - Introduce `woocommerce_coupon_data_panels` action. +* Dev - Add $package to is_available shipping method hooks. +* Dev - Add tool for disabling shipping rate cache for debug. + += 2.1.8 - 2014-04-30 = +* Fix - Prevent saving duplicate skus in quick edit. +* Fix - Sorting of downloads on my account page. +* Fix - Clear cached API reports when deleting other order transients. +* Fix - Shipping calculator cart messages. +* Fix - Display of UTF8 attributes on view order page. +* Fix - Changed the way the order review html is appended to the checkout page via JS to reduce likelihood of errors. +* Fix - Allow removing downloads from product by removing all rows. +* Fix - Ignore variation stock if disabled globally. +* Fix - Prevent duplicate admin menu items when using menu editor plugins. +* Fix - Remove title from product not purchasable message to prevent possible data leak. Thanks Julio Potier. +* Tweak - Updated REST API docs link. +* Tweak - Updated prettyphoto dependencies. +* Tweak - Customer search performance improvements. +* Tweak - Made default shipping label clearer. +* Tweak - Default order email to user email. +* Tweak - Only show downloadable item related text when product has downloads. +* Tweak - Improved Abstract product constructor. +* Tweak - Add COD instructions to emails. + += 2.1.7 - 2014-04-10 = +* Fix - Allow WC API to generate API keys for different user than the one that is making request. +* Fix - Fix the SKU search logic so it works with other filters. +* Fix - Correctly round shipping + shipping tax together when passes the tax inclusive total to paypal. +* Fix - orderby - skip adding hidden input of submit on a GET so JS can submit properly. +* Fix - Check wc_checkout_params.is_checkout against string '1' instead of int 1. +* Fix - Check order exists when resuming on checkout. +* Fix - When removing base taxes, round to precision. +* Fix - Ensure _order_currency is set. +* Fix - Use `$wpdb->db_version()` instead of `mysql_get_server_info()` deprecated in PHP 5.5. +* Fix - myaccount registration added check for auto generate password option. +* Fix - API: normalize both key and value before calculating OAuth signature. +* Fix - API: double-encode percent symbols when normalizing parameters. +* Fix - API: Remove post_parent so grouped simple products are also returned. +* Fix - Clear featured transients when needed. +* Fix - Stay on checkout when removing coupon. +* Fix - Prevent totals refreshing on every keydown event on the checkout. +* Fix - When hierarchy is off, only show children in the cat widget. +* Fix - Delete term count transients after stock status change and trashed post. +* Fix - During save_meta_boxes, only save for the "main" post being saved, not nested or subsequent save_post events. +* Fix - Stop _wc_session_expires autoloading. +* Fix - Remove nonce from comment form to prevent issues with caching. +* Fix - reset grouped products correctly to work with short codes. +* Fix - In admin, work out cart discount without tax amounts. +* Tweak - Apply filters to $product_type and we can set a default product type to new products. +* Tweak - wp_kses_post for meta display in admin. +* Tweak - woocommerce_order_cancelled_notice hook. +* Tweak - Use is_ssl() for get_woocommerce_api_url(). +* Tweak - Changes to filters to see if shipping is needed or not in the cart class. +* Tweak - Chunk option names in cleanup_sessions() to reduce load. +* Tweak - Change \WC_Order::add_order_note cap to edit_shop_order instead of manage_woocommerce. +* Tweak - Allow filtering order statuses in dashboard reports widget. +* Tweak - Added is_paying_customer() to easily check if a user is a WC customer. +* Tweak - Allow query string fallback for REST API SSL auth. +* Tweak - woocommerce_coupon_get_discount_amount filter in coupon class. +* Tweak - More friendly/less blunt "no shipping" messages. +* Tweak - use network_site_url instead of network_admin_url for multisite. +* Tweak - Updater - Only show upgrade notices, and use transient cache. +* Tweak - get_image_id method for use in email template. Shows correct variation images. +* Tweak - added validation when save the frontend colors. + += 2.1.6 - 2014-03-25 = +* Fix - Fixed a bug where cron events are scheduled using a function name rather than a hook name. +* Fix - Given transients not required on all pages expiration times to prevent autoloading. +* Fix - Don't trailingslash Order Cancel URLs with a Query String. +* Fix - Switch to jquery trim to allow checkout in older IE. +* Fix - Variation bulk sale price edit over reaching causing errors on save. +* Fix - Only append generator tag on HTML pages. +* Fix - AED currency symbol. +* Fix - Move loop_end hooks as it is generic and used in all WP loops. Prevents some theme conflicts. +* Fix - Lingering tooltip after gallery image delete. +* Fix - Move plugin headers to main WC POT file. +* Fix - Correct discount calculation in admin when fees are involved. +* Fix - Fix sale flash for out of stock sale items. +* Fix - Use protocol relative URLs in the cart widget because it gets cached and can display on https or http pages. +* Fix - Fix term recount during WP callbacks. +* Fix - Convert states to strings for PayPal (non-US). +* Fix - Hide empty at walker level to fix category widget display. +* Fix - form-login form values were not persistent after failed submission. +* Fix - URL decode not needed for custom text attribute names. +* Fix - Fix bulk editing variation sale price. +* Fix - Remove comment exclusion in order notes meta box. +* Fix - Sync min and max prices for regular and sale prices so prices are displayed correctly when sale price is lower than a regular price of another variation. +* Fix - Expanding line item_meta causes conflicts if attributes are named with things like 'name', 'type' or 'qty'. Added blacklist to exclude unsafe values. +* Fix - Added support for clearing report transients when using object caching. +* Fix - encoding issues with attribute values. +* Fix - Escape the contents of the changelog when displayed. +* Fix - Edge case where tax was still displayed for shipping when exempt. +* Tweak - Allow city field to use another input method. +* Tweak - Several new filters. +* Tweak - PayPal, modify currency error message to include both sent and returned currencies for comparison. +* Tweak - enable keyboard shortcuts in prettyPhoto. +* Tweak - Add classes to item meta. +* Tweak - Use is_purchasable to determine if a variation cart button is needed, and potentially show empty_price_html. +* Tweak - new woocommerce_cart_taxes_total filter. +* Tweak - new wc_cart_totals_taxes_total_html() function. +* Tweak - Use API request URL for mijireh and PayPal callbacks. +* Tweak - move variation data to tooltip in order items meta box. +* Tweak - Store variation data for order items added through the admin. +* Tweak - Billing Address > Billing Details text. We take more than address in this section. +* Tweak - Delete terms transient during recount. +* Refactor - jshint javascript files. +* Localisation - add Bangladeshi currency and symbol. +* Localisation - Bangladeshi states (districts). +* Localisation - Croatian currency symbol. + += 2.1.5 - 2014-03-06 = +* Fix - Prevent notices on new plain text email parameter for BACS and Cheque gateways +* Fix - Fixed issue where variation prices were hidden when variation stock management wasn't set +* Fix - Discounts fixed_product are now properly multiplied by quantity +* Fix - Fixed bulk edit % increase and decrease +* Fix - Extra check for access_expires when getting downloadable files +* Fix - get_related method fixed tags OR query not excluding product ID +* Tweak - Fallback for when add ons page is not loading +* Tweak - Hide price suffix in admin panel lists + += 2.1.4 - 2014-03-05 = +* Fix - Prevent duplicate loading of functions files +* Fix - Fixed breaking timeline for reports +* Fix - Category widget ordering +* Fix - BACS and Cheque gateway emails now have a plain text flag +* Fix - wc_get_product_ids_on_sale will never return 0 +* Fix - Prevent errors upon comments when order is in trash +* Fix - Reviews widget now links to proper review anchor +* Fix - Added support for permalinks containing a query string in wc_get_endpoint_url +* Fix - Variable product add file button works after adding a new variation +* Fix - Resolved issue where styles didn't get compiled properly +* Fix - Changed the save order so email data is correct for manual orders +* Fix - Later hooking in for template redirect +* Fix - Reverted load order change for language files and provided proper context +* Fix - Made woocommerce_update_cart_action_cart_updated a filter which can return true or false to recalc totals +* Fix - Updated sync logic to exclude hidden and out of stock variations from the price display +* Fix - Changed set_stock to only make a variable product out of stock if all variations are stock managed to resync prices after stock changes +* Fix - Use woocommerce_notify_no_stock_amount not 0 in variable class sync method +* Fix - Suppress errors in download handler by silencing ob_flush and flush calls +* Fix - load_textdomain first from WP_LANG_DIR before load_plugin_textdomain +* Tweak - get_states method now returning false instead of array for countries without states +* Tweak - Improved shipping language strings +* Tweak - Remove admin check around global po translation file loading +* Tweak - Improved styles for tab views +* Refactor - Hardened code base and fixed strict standards notices + += 2.1.3 - 2014-02-27 = +* Fix - Use correct thresholds to calculate out of stock number in dashboard widget +* Fix - Admin screen strings sanitised to work with Chinese characters +* Fix - REST API OAuth signature fixed when using filter params +* Fix - Ensure shipping address data for customers is updated if only shipping to billing address +* Fix - Fixed sprintf missing parameter in customer-new-account.php template +* Fix - Ampersand character properly outputted in plain text emails +* Fix - Stack the password reset fields on smaller screens +* Fix - Bulk edit options now reflect on all variations +* Fix - Prevent notices when country has states but no states are specified in cart +* Fix - Make my-account/view-order use the correct template file +* Fix - Prevent session warnings when WordPress logs out +* Fix - Prevent spaces in file names from breaking product thumbnails +* Fix - Resolved issue where download permissions where not granted in some cases upon order complete +* Fix - Prevent IE cursor from being stuck in loading animation +* Fix - Ensure wc_get_product_ids_on_sale returns array of ints, not strings +* Fix - Prevent rating stars from wrapping in IE +* Fix - Rating stars properly aligned in small screens +* Fix - Handle get_rate_code when no matching rate is found +* Fix - Cleaned noticed that might show when tax rates left empty +* Fix - Prevented warning when you have hide shipping methods until address entered selected in CoD +* Fix - Better URL detection for subdirectory installs in get_woocommerce_api_url +* Fix - Transient names are now md5 hashed so they don't exceed max length +* Fix - Fixed searching for orders and speed improvements there +* Fix - "Show children of current category only" option working again in Product Category widget +* Fix - Order search by SKU can now return multiple results again +* Fix - Fixed alignment of radio button in settings API +* Fix - Layered nav widget now shows terms again when using list OR +* Fix - Prevent multiple attempts at cancelling actions on orders +* Fix - Fixes in reporting for variations +* Fix - My Accounts downloads section now shows files names +* Fix - Improved code to log in via email address +* Fix - When guest checkout is enabled, manual made admin orders can be paid without logging in +* Fix - Fix product counts when subcategories are displayed +* Fix - Fixed Relevanssi conflicts +* Fix - Fixed issue with Mijireh incorrectly rounding shipping totals +* Fix - Shipping class selection will be hidden for virtual variations +* Fix - Prevent product percentage based coupons to stack discounts +* Tweak - Added United Arab Emirates Dirham currency +* Tweak - Enforce a static base for product permalink structures to prevent 404 issues +* Tweak - Introducing woocommerce_get_username_from_email filter in login processing method +* Tweak - Trash pages swapped for endpoints instead of force delete +* Tweak - Flip default status of price_trim_zeros (defaults to true now) +* Tweak - Performance improvements for My Account view with a lot of available downloads +* Tweak - Introduces woocommerce_return_to_shop_redirect filter +* Tweak - Improved tax rounding, to calculate an accurate tax total +* Tweak - Introduced failsafes to prevent actions trying to take place on removed products +* Tweak - Permalinks enabled data in REST API +* Tweak - Filters to restrict granting/revoking access to files +* Tweak - Added Canadian address format +* Refactor - Multiple docblock updates +* Refactor - Multiple code standards improvements +* Refactor - Changed all text strings without explicit domain, to use the 'woocommerce' text domain +* Refactor - Speed improvements in various places + += 2.1.2 - 2014-02-13 = +* Fix - Removed nl2br function from plain text email-order-items email template +* Fix - Made static string translatable in email-order-items email template +* Fix - Added missing third parameter to _doing_it_wrong call +* Fix - Sidebar in reports screen does now fit big numbers (long strings) +* Fix - Report stock icon properly set up +* Fix - Removed manual checks for AJAX requests, relying on DOING_AJAX constant now +* Fix - Checkout get_value now returns null if no value is set, default can be used as fallback +* Fix - Variation download url now cleaned via wc_clean instead of esc_url_raw +* Fix - Wrap billing and shipping fields with a div/class to prevent field order issues +* Fix - Fix line total display for order fees in admin panel order view page +* Fix - Restored multiple image selection for product gallery +* Fix - Update schemas before DBDELTA to fix mysql errors on update +* Fix - Italian address formatting fix +* Fix - Set countries as an array by default in the shipping abstract +* Fix - Fixed term counts when terms span taxonomies +* Fix - Fixed saving of fee tax total +* Fix - Fixed "Shipping via" label in PayPal +* Tweak - Tweak the order of checks in bulk_and_quick_edit_save_post to make it more efficient on autosave +* Tweak - Always set order billing email address when user is logged in, if no email is provided +* Refactor - Removed obsolete view order shortcode class +* Localization - Multiple localization updates + += 2.1.1 - 2014-02-12 = +* Feature - Show notice if template files are out of date for themes including WooCommerce template files +* Feature - Introducing supporting is_wc_endpoint_url function +* Fix - During install, register all post types and endpoints so that the rewrite rules are correctly generated. +* Fix - Allow line breaks in customer addresses on order details page +* Fix - Fixed all language country codes to reflect WordPress standards +* Fix - Payment gateway section links work in lower and upper caps texts +* Fix - Prevents nonce notice when removing item from cart +* Fix - Hide empty categories in product_categories shortcode +* Fix - Fixed Twenty Thirteen single product page layout +* Fix - Fix saving of checkboxes (off state) in widgets API +* Fix - Proper password validation in user register on the My Account page +* Fix - When add_to_cart is called, ensure the correct product_id is set for variations +* Fix - Restored Italian translation files +* Fix - Ensure stock status is updated. Prevents new products being hidden when the option to hide out of stock products is enabled +* Fix - Fix manual order calculation when using non-standard decimal points +* Tweak - Added Croatian Kuna currency +* Tweak - Throw a non-fatal notice when file trying to be included as template doesn't exist +* Tweak - Add versions to all scripts + styles to ensure browser cache is cleared +* Tweak - Added tinymce buttons relevant to the short description +* Refactor - Removed unused change password template and shortcode class +* Refactor - Several function dockblocks improved +* Refactor - Stripped out some unused variables + += 2.1.0 - 2014-02-10 = +* Feature - New REST API +* Feature - Define whether prices should be shown incl. or excl. of tax, and add an optional suffix. +* Feature - Show grouped or itemized taxes during checkout. +* Feature - Split frontend styles into separate appearance/layout stylesheets and removed the enable/disable option. +* Feature - Added woocommerce-smallscreen.css to optimise default layout on handheld devices. +* Feature - Bulk edit increase / decrease variation prices by fixed or percentage values +* Feature - Admin action to link past orders of the same email address to a new user. +* Feature - Account edit page for editing profile data such as email. +* Feature - Customer list reports. +* Feature - Reports - New design, export csvs, more data. +* Feature - Ability to link past orders to a customer (before they registered). +* Feature - Authorize option for PayPal Standard. +* Feature - Separate options for countries you can ship to and sell to. +* Feature - BACS supports multiple account details. +* Feature - PayPal PDT support (as alternative to IPN). +* Feature - Handling for password protected products. +* Feature - Schema markup selector for downloadables. +* Feature - woocommerce_get_featured_product_ids function. +* Feature - WC_DELIMITER to customise the pipes for attributes +* Feature - Standardized, default credit card form for gateways to use if they support 'default_credit_card_form'. +* Feature - Coupon usage limits per user (using email + ID). +* Feature - Option to limit reviews to purchasers. +* Feature - Option to install missing WooCommerce pages from tools page. +* Feature - New notices API for adding errors/notices +* Feature - Compatible with WordPress 3.8 default theme 'TwentyFourteen'. +* Feature - Added is_store_notice_showing conditional. +* Feature - Allow gateways to change the checkout place order button text on selection. +* Tweak - Added pagination to tax rate screens. +* Tweak - Added filter to check the 'Create account' checkbox on checkout by default. +* Tweak - Update CPT parameters for 'product_variation' and 'shop_coupon' to be no longer public. +* Tweak - COD processing instead of on-hold. +* Tweak - Added filter to explicitly hide terms agreement checkbox. +* Tweak - New System Status report layout, now plugin list is better visually and very better to read. +* Tweak - content-widget-product.php template for product lists inside core widgets. +* Tweak - Shipping is now renamed to Shipping and Handling on checkout. +* Tweak - Select all/none for countries in admin. +* Tweak - Handling for multiselect fields on checkout, and a filter for third party handling. +* Tweak - Made scripts/styles use protocol-relative URLs. +* Tweak - Revised shiptobilling functionality on the checkout. "ship to different address" option used instead. +* Tweak - Filterable page installer. +* Tweak - Order details optimised for small screens. +* Tweak - Streamlined account process - username and passwords are optional and can be automatically generated. +* Tweak - Updated/new dummy data (including .csv files to be used with [Product CSV Import Suite](https://woocommerce.com/products/product-csv-import-suite/)). +* Tweak - Product shortcodes columns parameter now affects layout correctly. +* Tweak - Disabled button styles. +* Tweak - Hooks for overriding default email inline styles. +* Tweak - Flat rate shipping support for percentage factor of additional costs. +* Tweak - local delivery _ pattern matching for postcodes. e.g. NG1___ would match NG1 1AA but not NG10 1AA. +* Tweak - Improved layered nav OR count logic +* Tweak - Make shipping methods check if taxable, so when customer is VAT excempt taxes are not included in price. +* Tweak - Coupon in admin bar new menu #3974 +* Tweak - Shortcode tag filters + updated menu names to make white labelling easier. +* Tweak - Removed placeholder polyfill. Use this plugin to replace functionality if required: https://wordpress.org/plugins/html5-placeholder-polyfill/ +* Tweak - Replaced all instances of → and ← in frontent using wc icon font plus .wc-forward and .wc-backward utility classes. +* Tweak - Add review form no longer opens in lightbox. +* Tweak - Move average rating outside of hidden tab for google #3867. +* Tweak - Add formatted_woocommerce_price filter. +* Fix - Changed MyException to Exception in Checkout class as MyException class does not exist in WooCommerce +* Fix - Default cart widget styling on non-wc pages. +* Fix - Rounding for mijireh tax ex. price. +* Fix - Updated blockui to prevent errors in WP 3.6. +* Fix - Tweaked popularity sorting to work better when no sales are present. +* Fix - Quote encoding in email subjects. +* Fix - Added $wp_error parameter during checkout process to ensure WP_Error object is returned on error and checkout process is properly stopped. +* Refactor - Taken out Piwik integration, use https://wordpress.org/plugins/woocommerce-piwik-integration/ from now on. +* Refactor - Taken out ShareYourCart integration, use https://wordpress.org/plugins/shareyourcart/ from now on. +* Refactor - Moved woocommerce_get_formatted_product_name function into WC_Product class. +* Refactor - Improved parameter handling in woocommerce_related_products() function. +* Refactor - Widget classes (added abstract and combined similar widgets). +* Refactor - Removed pay and thanks pages. Endpoints are used instead. +* Refactor - Removed certain my-account pages. Endpoints are used instead. +* Localization - Portuguese locale by jpBenfica. +* Localization - Swedish translation by Björn Sennbrink. +* Localization - Japanese translation by Shohei Tanaka. +* Localization - Danish translation by Mikael Lyngvig. +* Localization - Spanish translation by Luis Giménez. +* Localization - French, Spanish, Romanian, Danish, Korean, Czech, Arabic, Hungarian updates. + += 2.0.20 - 2013-11-21 = +* Tweaked paypal request +* Check for WP Error when getting terms in breadcrumb file +* Sanitize when searching in admin +* Fix yard to cm conversion + += 2.0.19 - 2013-11-04 = +* Fix - get_item_subtotal() logic +* Fix - Pass number of products variable to get_related for more then 5 related products +* Fix - Email fatal error for orders with missing products +* Fix - Local pickup base tax option on first calculation +* Tweak - For paying customer column, use a dash #3971 +* Tweak - Added wordwrap to order notes +* Localisation - Updated Portuguese, Slovak, French, Lithuanian, Finnish + += 2.0.18 - 2013-10-21 = +* Fix - Escaped the "hide-wc-extensions-message" link in admin. +* Fix - CSS -mox- prefix. #3953 +* Fix - Remove sorting args after main query #3969 +* Tweak - Pass index to woocommerce_save_product_variation. #3962 +* Tweak - woocommerce_variable_product_sync hooks +* Tweak - CH postcode validation +* Tweak - Show layered nav widgets on any product taxonomy page +* Localisation - Various corrections + += 2.0.17 - 2013-10-17 = +* Fix - Add missing doctype for email header #3921 +* Fix - 2 notices on the cart/checkout related to tax #3922 +* Fix - Allowed more tags to be saved in sharethis code +* Fix - If no products on sale, don't show results in sale shortcode +* Fix - During remove_product_query, remove ordering filters to prevent affecting other queries +* Tweak - Min amount check takes taxes in consideration #3924 +* Tweak - Added validate_ID_field method check to settings API for special validation rules +* Tweak - Added needs_payment() method. Checks if an order needs payment, based on status and order total. +* Tweak - Key value pairs for order_meta (woocommerce_email_order_meta_keys) +* Tweak - Added wc_variation_form trigger to variations javascript for 3rd party plugins + += 2.0.16 - 2013-10-14 = +* Fix - woocommerce_change_term_counts needs to always return terms. Fixes category display. +* Fix - Attribute label display. +* Fix - add_to_cart shortcode correct use of setup_product_data + += 2.0.15 - 2013-10-14 = +* Fix - Added missing line break in plain text email. +* Fix - Strict standards warnings in category walkers +* Fix - Remember which attributes get registered to minimize conflicts +* Fix - Allow quotes in store name for display in emails +* Fix - Regression bug throwing warning in ShareThis integration +* Fix - Correct state code for Canadian state Newfoundland +* Fix - Tweaked popularity sorting to work better when no sales are present. +* Fix - Removed action from single add to cart templates to resolve issues with other plugins. +* Fix - Unsetting active classes #3896 +* Fix - update_status checks if the order id exists #3904 +* Fix - the first matching variation should be used (replaced pop() with shift()) @eef86ab +* Fix - get_children should get published products only for grouped products #3880 +* Fix - Clean the SKU, prevents variations breaking when " was saved @dc6574b +* Fix - Correctly check attribute label and name upon creation @31c34f6 +* Fix - Fixed Multiple Sets of Tabs on One Page @e584ea8 +* Fix - Add tax_rate_id to objects returned from cart->tax_totals @40c85ec +* Fix - find_product_in_cart check if cart is array #3863 +* Fix - bump_request_timeout strict standards @4798cb8 +* Fix - Fix upload dir #3812 +* Fix - Remove accents from taxonomy names + run through filters #3832 +* Tweak - Always show returning customer login box. + += 2.0.14 - 2013-09-05 = +* Tweak - Update cart performance improvements +* Fix - Google Analytics no longer identifies users using custom vars +* Fix - Send tax inclusive, rounded item price to Google Analytics +* Fix - Use version_compare to check for required jQuery +* Fix - Made gateway abstract compatible with implementations to prevent strict notices +* Fix - Update order's GMT date ('post_date_gmt') when changing the order date via the "Edit Order" screen +* Fix - Hardened the checkout payment URL method +* Fix - Regression bug fixed, allowing 0 value attributes again +* Fix - API url function work when permalinks are not pretty +* Fix - Chosen select boxes now support RTL languages +* Fix - Refresh when creating an account to prevent nonce issues +* Other minor fixes + += 2.0.13 - 2013-07-19 = +* Tweak - Allow users with edit rights to add draft products to cart (and nobody else) +* Tweak - Handle pending status for paypal +* Tweak - Only refresh fragment when cart cookie > 0 +* Tweak - Updated/new dummy data (including .csv files to be used with [Product CSV Import Suite](https://woocommerce.com/products/product-csv-import-suite/)). +* Fix - Extra escaping on layered nav variables to prevent injection +* Fix - Improved sanitization of option fields +* Fix - Add fee total to cart total +* Fix - Flush rewrite rules after adding or editing attributes +* Fix - Set session after removing item from cart to prevent issues after removing last item +* Fix - Sale expiration now works for variations as well +* Fix - httpversion 1.1 for paypal upcoming changes +* Fix - Price filter widget: preserve orderby +* Fix - Fix paypal phone mask (whitespace) +* Fix - Correct sanitization of option fields +* Fix - Sanitized shipping calc form to fix persistent XSS issue. +* Localisation - ES States + += 2.0.12 - 2013-06-17 = +* Tweak - Add actions for attribute create/update/delete +* Fix - Fixed bug in cross sells loading in product data write panel +* Fix - Fixed posting shipping method when only one is available +* Fix - Fixed query breaking when using some product widgets + += 2.0.11 - 2013-06-13 = +* Tweak - Handling for multiselect fields on checkout, and a filter for third party handling +* Fix - Duplicate param keys for sale_product shortcodes +* Fix - Google Analytics tracking use get_order_number() method instead of id +* Fix - Replaced jQuery placeholder plugin to provide support in older browsers +* Fix - Rounding for mijireh tax ex. price +* Fix - Fixed is_on_sale function for products without prices +* Fix - Updated blockui to prevent errors in WP 3.6 +* Fix - Extra data sanitization in some places +* Fix - Offer tax class option per variation to use same tax class as parent + += 2.0.10 - 2013-05-15 = +* Tweak - Searching for SKU in admin panel can also be done via lowercase 'sku:' instead of just 'SKU:' +* Fix - Cast term_id as int in product data write panel that will resolve issues with numerical attributes +* Fix - Correct label for RUB symbol - added a dot after it +* Fix - Javascript escapes to stop breaking scripts when used with translations +* Fix - PayPal button should use classes 'button' and 'alt', not 'button-alt' +* Fix - Have the remove_taxes() method set subtotal to subtotal_ex_tax +* Fix - Allow layered nav to work with non pa_ prepended taxonomies +* Fix - Better backwards compatibility with _woocommerce_exclude_image +* Fix - is_on_sale() method now returns true for products with a sale product of 0 +* Fix - For when get_the_terms() returns false inside woocommerce_get_product_terms() +* Fix - PayPal has a 9 item limit +* Fix - Replace deprecated wp_convert_bytes_to_hr() with size_format() + += 2.0.9 - 2013-05-02 = +* Feature - Added is_product_taxonomy() conditional. +* Tweak - Notices during checkout for admin users if the checkout is mis-configured. +* Tweak - Named charts on report page to make modifications easier. +* Tweak - woocommerce_before_delete_order_item hook. +* Fix - Disable autocomplete for checkout fields which refresh totals - no events get fired on autocomplete. +* Fix - Clear rating transients when editing comments. +* Fix - Screen ids when plugin name localised. +* Fix - Brazilian state code BH -> BA. Data update required to update old values in orders. +* Fix - Fix incorrect CSS class being output in product image gallery. +* Fix - Mijireh page slurp. +* Fix - woocommerce_downloadable_product_name filter fixes. +* Fix - Pass order number to google analytics, not id +* Fix - check_jquery in WP 3.6 beta +* Fix - GA click tracking moved code to footer. +* Localization - Netherlands, Hungarian, Taiwan, Italian, CZ, Spanish, Catalan updates. +* Localization - Slovak translation by Dusan Belescak. +* Localization - Added RUB currency. +* Other minor fixes and localisation updates. + += 2.0.8 - 2013-04-17 = +* Feature - Related products shortcode. +* Tweak - Order item meta - skip serialized fields. +* Tweak - Support for the city field in shipping calc (filterable). +* Tweak - use base_country for tax calculations in manually created orders. +* Tweak - Download permissions meta box show cleaner filenames. +* Fix - Updated shareyourcart SDK. +* Fix - moved woocommerce_get_filename_from_url to core-functions as it is required in admin too. +* Fix - checkmark after adding to cart multiple times. +* Fix - Saving text attributes. Posted 'text' terms are not slugs. Only striptags/slashes - don't change to slugs. +* Fix - Insert URL button when working with multiple variations. +* Fix - Undefined found_shipping_classes in flat rate shipping. +* Fix - Fix saving options for attribute taxonomies containing special chars. +* Fix - Prevent empty meta queries. +* Localization - Norwegian updates by Tore Hjartland +* Localization - Spanish updates by Laguna Sanchez +* Localization - Romanian updates by Aurel Roman +* Localization - Finnish updates by arhipaiva + += 2.0.7 - 2013-04-12 = +* Feature - Option for GA _setDomainName. +* Tweak - Removed rounding when option to round at subtotal is set. +* Fix - Allow extra flat rate options even if main rate is 0. +* Fix - Fix email subject lines if options not set. +* Fix - Prevent over-sanitization of attribute terms when editing products. +* Fix - Sanitize terms when linking all variations. +* Fix - Sanitize coupon code names before checking/applying. + += 2.0.6 - 2013-04-10 = +* Tweak/Fix - Merge taxes by CODE so totals are displayed clearer. Also added additional function for getting merged tax totals, and to keep compatibility with themes. +* Tweak/Fix - Recent reviews show actual review stars, and allowed get_rating_html() to be passed a rating. Also removed unused $location var. +* Fix - Saving of meta values from paypal after payment. +* Fix - woocommerce_nav_menu_items - only hide pages, not other objects. +* Fix - woocommerce_add_tinymce_lang array key. +* Fix - Find_rates now works with both postcode and city together. +* Fix - PrettyPhoto content clearfixed. +* Fix - Fix the download method when force SSL is on. +* Fix - Put back sandbox pending fix. Apparently still needed for some accounts. +* Fix - Do not sanitize old attribute name to not mess up comparing +* Fix - Settings API empty value only used if set. In turn fixes blank values in flat rate shipping. +* Fix - Ensure API Request URL scheme is not relative to the current page. +* Fix - Fix saving of download permissions in order admin. +* Fix - Action woocommerce_product_bulk_edit_end is now properly executed instead of outputted as HTML. +* Fix - Fix IE Download via SSL bug and fix http file over SSL. +* Fix - Show non-existing product line items. +* Fix - Conflicts with W3 Total Cache DB Cache +* Fix - piwik tracking. +* Tweak - Added a check to parent theme for comments_template before loading plugin template. +* Tweak - Remove hard coded max from random products widget. +* Tweak - Add filter hook to the place order button for easy 3rd party manipulation. +* Tweak - UX - Placeholder fades out on focus +* Tweak - UX - Only display validation result on required fields +* Tweak - Product column widths in admin +* Tweak - .shipping_address clears to avoid flash of ugliness in some themes when revealing shipping address +* Tweak - created an icon font for the star ratings to improve consistency +* Tweak - woocommerce_show_page_title filter +* Tweak - wrapper / css tweaks for TwentyThirteen compatibility +* Tweak - Added filters for controlling cross-sell display +* Tweak - Made hierarchy code in breadcrumbs more reliable. +* Localisation - NZ States +* Other minor fixes and localisation updates. + += 2.0.5 - 2013-03-26 = +* Tweak - Made no shipping available messages filterable via woocommerce_cart_no_shipping_available_html and woocommerce_no_shipping_available_html. +* Tweak - disabled keyboard shortcuts in prettyPhoto. +* Tweak - woocommerce_date_format() function. +* Tweak - After adding to cart, add 'added_to_cart' to querystring - lets messages show with cache enabled. +* Tweak - Similar to above, on failure don't redirect. The POST should exclude from cache. +* Tweak - Version data on system status page. +* Fix - Fix orderby title - separated from menu_order. +* Fix - WC_Product::set_stock_status() to correctly set status. +* Fix - last_modified_date updated on status change for orders. +* Fix - Sanitize id in woocommerce_get_product_to_duplicate function +* Fix - Cancel order function now looks at post_modified instead of post_date. +* Other minor fixes and localisation updates. + += 2.0.4 - 2013-03-18 = +* Tweak - Like my account, added order_count attribute to view order shortcode. +* Tweak - Moved WC_Order_Item_Meta into own file. +* Tweak - PayPal Standard gateway - no longer needs sandbox fix, and notify-validate should be first in the requests. +* Tweak - Flat rate interface tidy up. +* Tweak - Add order_id to woocommerce_download_product hook +* Tweak - Disabled prettyPhoto deeplinking +* Tweak - Applied a width to the product name column (edit products) to fix layout small screens +* Tweak - Filters for attribute default values. +* Tweak - Added filter to control order stock reduction when payment is complete. +* Tweak - Increase priority of woocommerce_checkout_action and woocommerce_pay_action so things can be hooked-in prior. +* Tweak - Tweaked default locale to include all fields so that checkout fields can fallback if specific properties are not set e.g. required. +* Tweak - Removed Base Page Title option - rename the page instead. +* Fix - WC_Order_Item_Meta support for keys with multiple values. +* Fix - Codestyling bug with meta.php +* Fix - Icon replacement in .woocommerce-info for Gecko +* Fix - prettyPhoto next/prev links and thumbnail navigation no longer appear when there's only one attachment +* Fix - Attribute base +* Fix - Fixed adjust_price method in product class, allowing negative adjustments +* Fix - Ratings and rating count transient syncing. +* Fix - Tax label vs name in order emails. +* Fix - Sendfile when FORCE_SSL_ADMIN is enabled. +* Fix - "for" attribute within product_length option. +* Fix - Encode the URLs generated by layered nav widget. +* Fix - Order Again for variations. +* Fix - Preserve arrays in query strings when using orderby dropdown. +* Fix - Move track pageview back to template_redirect to prevent headers_redirect, but give a later priority than canonical. +* Fix - Product tabs when a product type doesn't exist yet. +* Fix - Saving of variation download paths with special chars. +* Fix - Unset parent of children when deleting a grouped product. +* Fix - Removed Sidebar Login Widget. Use https://wordpress.org/extend/plugins/sidebar-login/ instead. A potential security issue was found regarding logging of passwords (since GET was used instead of POST). Sidebar Login 2.5 resolves this and the widget has been removed from WC to prevent needing to maintain two (virtually identical) codebases. +* Localization - Added indian rupees +* Localization - Updated French translation by absoluteweb +* Localization - Updated Brazilian translation by Claudio Sanches +* Localization - Updated Hungarian translation by béla. + += 2.0.3 - 2013-03-11 = +* Feature - Added products by attribute shortcode, e.g. [product_attribute attribute="color" filter="blue"] +* Tweak - Made coupon label more clear. +* Tweak - woocommerce_cart_redirect_after_error hook. +* Tweak - woocommerce_cancel_unpaid_order hook to control if an order should be cancelled (if unpaid) +* Tweak - woocommerce_valid_order_statuses_for_payment and woocommerce_valid_order_statuses_for_cancel hooks for pay pages/my account. +* Tweak - WC_START in checkout json requests to prevent notices breaking checkout. +* Tweak - Add filters to product images and thumbnails. +* Tweak - IPN email mismatch puts order on-hold. +* Tweak - Option to set main paypal receiver email. +* Tweak - Download file links show filename as part of link. +* Fix - Samoa -> Western Samoa +* Fix - Re-applied image setting tooltips +* Fix - Post code ranges (taxes) on insert. +* Fix - Moved init checkout to a later hook to prevent canonical template redirects kicking in. +* Fix - Made custom attributes more robust by using sanitized values for variations. +* Fix - woocommerce_cancel_unpaid_orders respects the manage stock setting. +* Fix - Mijireh Page Slurp. +* Fix - Removed unused 'woocommerce_prepend_shop_page_to_urls' setting from breadcrumbs. +* Fix - hide_cart_widget_if_empty option. +* Fix - Added legacy paypal IPN handling. +* Localization - Finnish translation by Arhi Paivarinta. + += 2.0.2 - 2013-03-06 = +* Fix - Frontpage shop when 'orderby' is set. +* Fix - Fix add-to-cart for grouped products which are sold individually. +* Fix - Payment method animation on the checkout. +* Fix - Updated chosen library. +* Fix - Saving of attributes/variations with custom product-level attributes. +* Fix - Include once to prevent class exist errors with widgets. +* Fix - Fixed welcome screen bug shown in updater frame +* Fix - Upgrade if DB version is lower than current. +* Fix - FROM prices now ignore blank strings for variations. +* Fix - Ensure order contents are saved before mailing via admin interface. + += 2.0.1 - 2013-03-04 = +* Fix - Added an extra permalink flush after upgrade to save needing to do it manually. + += 2.0.0 - 2013-03-04 = +* Feature - Sucuri audited and secured. +* Feature - Added sales by category report. +* Feature - Added sales by coupon report (kudos Max Rice). +* Feature - Multiple downloadable files per product/variation (kudos Justin Stern). +* Feature - Download expiry for variations (kudos niravmehta). +* Feature - Added wildcard support to local delivery postcodes. +* Feature - Option to enable Cash on Delivery for select shipping methods only. +* Feature - Stopped using PHP sessions for cart data - using cookies and transients instead to allow WC to function better with static caching. Also to reduce support regarding hosts and session configurations. +* Feature - Export and Import Tax Rates from a CSV file. +* Feature - Option to control whether tax is calculated based on customer shipping or billing address. +* Feature - New options for individual transaction emails with template editor. +* Feature - Added "On Sale" shortcode (thanks daltonrooney). +* Feature - Added "Best Selling" shortcode. +* Feature - Added "Top Rated" shortcode. +* Feature - Local pickup has the option to apply base tax rates instead of customer address rates. +* Feature - New product images panel to make working with featured images + galleries easier. +* Feature - Schedule sales for variations. +* Feature - Expanded bulk edit for prices. Change to, increase by, decrease by. +* Feature - Set attribute order (globally, per attribute). +* Feature - Allow setting the product post type slug to a static (non-translatable) text, mainly to be used for translating and WPML setups. +* Feature - Added lost password shortcode / email notification (thanks Max Rice). +* Feature - Simplified permalink/base settings now found in Settings > Permalinks. +* Feature - Support more permalink structures (from https://codex.wordpress.org/Using_Permalinks) +* Feature - Added option to resend order emails, checkboxes select which one. +* Feature - New layered nav current filters widget. This lists active filters from all layered nav for de-selection. +* Feature - Added the option to sell products individually (only allow 1 in the cart). +* Feature - New shop page/category archive display settings, and the ability to change display per-category. +* Feature - Allow shipping tax classes to be defined independent of items. https://github.com/woocommerce/woocommerce/issues/1625 +* Feature - Redone order item storage making them easier (and faster) to access for reporting, and querying purchases. Huge performance gains for reports. Order items are no longer serialised - they are stored in their own table with meta. Existing data can be be updated on upgrade. +* Feature - Update weights/dimensions for variations if they differ. +* Feature - is_order_received_page() courtesy of Lee Willis. +* Feature - Inline saving of attributes to make creating variable products easier. +* Feature - Zip code restriction for local pickup. +* Feature - New free shipping logic - coupon, min-amount, Both or Either. +* Feature - Taxes can be based on shipping, billing, or shop base. +* Feature - Filter coupons in admin by type. +* Feature - Append view cart link on ajax buttons. +* Feature - Revised the way coupons are stored per order and added new coupon reports on usage. +* Feature - Updated/new dummy data (including .csv files to be used with [Product CSV Import Suite](https://woocommerce.com/products/product-csv-import-suite/)). +* Feature - Option to hold stock for unpaid orders (defaults to 60mins). When this time limit is reached, and the order is not paid for, stock is released and the order is cancelled. +* Feature - Added set_stock() method to product class. +* Feature - Linking to mydomain.com/product#review_form will now open the review form on load (if WooCommerce lightbox is turned on) +* Feature - Customers can sort by popularity + rating. +* Feature - Option to exclude coupons from sale items (thanks aj-adl) +* Feature - Logout "page" which can be added to menus. +* Templating - Revised pagination, sorting areas (sorting is now above products, numbered pagination below) and added a result count. +* Templating - email-order-items.php change get_downloadable_file_url() to get_downloadable_file_urls() to support multiple files. +* Templating - loop-end and start for product loops, allow changing the UL's used by default to something else. +* Templating - woocommerce_page_title function for archive titles. +* Templating - CSS namespacing changes (courtesy of Brian Feister). +* Templating - My account page broken up into template files (by Brian Richards) +* Templating - CSS classes standardised. Instances of '.woocommerce_' & '.wc-' replaced with '.woocommerce-' +* Templating - Ratings added to loop. Remove with [this snippet](https://gist.github.com/4518617). +* Templating - Replaced Fancybox with prettyPhoto +* Templating - loop-shop which was deprecated is now gone for good. +* Templating - Renamed empty.php to cart-empty.php to make clearer. +* Templating - Renamed sorting.php to orderby.php to better reflect contained hooks and code. +* Templating - Product tabs rewritten - new filter to define tab titles, priorities, and display callbacks. +* Templating - loop/no-products-found.php template added. +* Tweak - Sorting uses GET to make it cache friendly +* Tweak - Optimised class loading (autoload). Reduced memory consumption. +* Tweak - Moved shortcodes and widgets to classes. +* Tweak - Tweaks to gateways API. Must use WC-WPI for IPN requests (classes will only be init when needed). +* Tweak - Save hooks for gateways have changed to match shipping methods. Plugins must be updated with the new hook to save options. +* Tweak - Cron jobs for scheduled sales. +* Tweak - Improved product data panels. +* Tweak - Improved installation + upgrade process upon activation. +* Tweak - Protect logs and uploads with a blank index.html +* Tweak - Append unique hash to log file names +* Tweak - get_order_number support for PayPal (thanks Justin) +* Tweak - Taxes - removed woocommerce_display_cart_taxes option in favour of never showing tax until we know where the user is (for tax exclusive prices). Tax inclusive continues to use base so prices remain correct. +* Tweak - Taxes - tweaked display of tax when using inclusive pricing to avoid confusion. +* Tweak - Taxes - improved admin interface and simplified options. +* Tweak - More granular capabilities for admin/shop manager covering products, orders and coupons. +* Tweak - Added some calculations to the order page when manually entering rows. Also added accounting.js for more accurate rounding of floats. +* Tweak - Order page can now calculate tax rows for you. +* Tweak - Display tax/discount total for reference on orders +* Tweak - Humanised order email subjects/headings +* Tweak - Cleaned up the tax settings. +* Tweak - If a PayPal prefix is changed, IPN requests break for all existing orders - fixed. new wc_get_order_id_by_order_key() function added. Thanks Brent. +* Tweak - On add to cart success, redirect back. +* Tweak - Prefix jquery plugins JS. +* Tweak - Made paypal use wc-api for IPN. +* Tweak - Due to new session handling, removed session section from the status page. +* Tweak - Removed upsell limit - show whats defined. +* Tweak - Args for upsells to control per-page and cols. +* Tweak - Recoded add_to_cart_action for better handling of variations and groups. +* Tweak - Admin sales stats show totals. +* Tweak - product_variations_{id} changed to product_variations array +* Tweak - Coupon description field. +* Tweak - Exclude up-sells from related products. +* Tweak - Allowed sku search to return > 1 result. +* Tweak - If only one country is enabled, don't show country dropdown on checkout. +* Tweak - Case insensitive coupons. +* Tweak - Made armed forces 'states' under the US rather than in their own 'country'. +* Tweak - Extended woocommerce_update_options for flexibility. +* Tweak - Added disabled to settings API. +* Tweak - Flat rate shipping - if no rules match, and no default is set, don't return a rate. +* Tweak - custom_attributes option added to woocommerce_form_field args. Pass name/value pairs. +* Tweak - Added html5 type inputs to admin with inline validation. +* Tweak - Use WP Core jquery-ui-slider +* Tweak - Further optimisation of icons in admin for HiDPI devices +* Tweak - On product search include post_excerpt, by krbvroc1 +* Tweak - Attribute page restricts reserved terms by GeertDD +* Tweak - Arguments for taxonomies are now filterable +* Fix - Added more error messages for coupons. +* Fix - Variation sku updating after selection. +* Fix - Active plugins display on status page. +* Localization - Makepot added by Geert De Deckere for generating POT files. +* Localization - Admin/Frontend POT files to reduce memory consumption on the frontend. +* Localization - French update by Arnaud Cheminand and absoluteweb. +* Localization - Romanian update by silviu-bucsa and a1ur3l. +* Localization - Dutch updates by Ramoonus. +* Localization - Swedish updates by Mikael Jorhult. +* Localization - Localized shortcode button. +* Localization - Norwegian translation by frilyd. +* Localization - Italian update by Giuseppe-Mazzapica. +* Localization - Korean translate by Woo Jin Koh. +* Localization - Bulgarian update by Hristo Pandjarov. +* Localization - Spanish update by bolorino. +* Localization - Finnish translation by Arhi Paivarinta. +* Localization - Chinese (Taiwan) translation by Fliper. +* Localization - Brazilian update by Fernando Daciuk. +* Localization - Hungarian translation by béla. +* Localization - Indonesian translation by Stanley Caramoy. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..9d5e431f0df --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@woocommerce.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 36fa9dea0ce..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,39 +0,0 @@ -# How to contribute - -Community made patches, localisations, bug reports and contributions are always welcome and are crucial to ensure WooCommerce remains the #1 eCommerce platform for WordPress ;) - -When contributing please ensure you follow the guidelines below so that we can keep on top of things. - -__Note:__ - -GitHub is for *bug reports and contributions only* - if you have a support question or a request for a customization don't post here. Use [WooThemes Support](http://support.woothemes.com) for customer support, [WordPress.org](http://wordpress.org/support/plugin/woocommerce) for community support, and for customisations we recommend one of the following services: - -- [Codeable](https://codeable.io/) -- [Elto](https://www.elto.com/) -- [Affiliated Woo Workers](http://www.woothemes.com/affiliated-woo-workers/) - -## Getting Started - -* Make sure you have a [GitHub account](https://github.com/signup/free) -* Submit a ticket for your issue, assuming one does not already exist. - * Clearly describe the issue including steps to reproduce when it is a bug. - * Make sure you fill in the earliest version that you know has the issue. - -## Making Changes - -* Fork the repository on GitHub. -* Make the changes to your forked repository. - * **Ensure you stick to the [WordPress Coding Standards](http://codex.wordpress.org/WordPress_Coding_Standards).** - * Ensure you use LF line endings - no crazy windows line endings. :) -* When committing, reference your issue (#1234) and include a note about the fix. -* Push the changes to your fork and submit a pull request on the master branch of the WooCommerce repository. Existing maintenance branches will be maintained of by WooCommerce developers. -* Please don't modify the changelog, this will be maintained by WooCommerce developers. - -At this point you're waiting on us to merge your pull request. We'll review all pull requests, and make suggestions and changes if necessary. - -# Additional Resources - -* [General GitHub documentation](http://help.github.com/) -* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) -* [WooCommerce Docs](http://docs.woothemes.com/) -* [WooThemes Support](http://support.woothemes.com) diff --git a/Gruntfile.js b/Gruntfile.js index 0f3f549287f..a756cc3f866 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,9 +1,10 @@ /* jshint node:true */ -module.exports = function( grunt ){ +module.exports = function( grunt ) { 'use strict'; grunt.initConfig({ - // setting folder templates + + // Setting folder templates. dirs: { css: 'assets/css', fonts: 'assets/fonts', @@ -11,42 +12,43 @@ module.exports = function( grunt ){ js: 'assets/js' }, - // Compile all .less files. - less: { - compile: { - options: { - // These paths are searched for @imports - paths: ['<%= dirs.css %>/'] - }, - files: [{ - expand: true, - cwd: '<%= dirs.css %>/', - src: [ - '*.less', - '!woocommerce-base.less', - '!mixins.less' - ], - dest: '<%= dirs.css %>/', - ext: '.css' - }] - } + // JavaScript linting with JSHint. + jshint: { + options: { + jshintrc: '.jshintrc' + }, + all: [ + 'Gruntfile.js', + '<%= dirs.js %>/admin/*.js', + '!<%= dirs.js %>/admin/*.min.js', + '<%= dirs.js %>/frontend/*.js', + '!<%= dirs.js %>/frontend/*.min.js', + 'includes/gateways/simplify-commerce/assets/js/*.js', + '!includes/gateways/simplify-commerce/assets/js/*.min.js' + ] }, - // Minify all .css files. - cssmin: { - minify: { - expand: true, - cwd: '<%= dirs.css %>/', - src: ['*.css'], - dest: '<%= dirs.css %>/', - ext: '.css' - } + // Sass linting with Stylelint. + stylelint: { + options: { + configFile: '.stylelintrc' + }, + all: [ + '<%= dirs.css %>/*.scss', + '!<%= dirs.css %>/select2.scss' + ] }, // Minify .js files. uglify: { options: { - preserveComments: 'some' + ie8: true, + parse: { + strict: false + }, + output: { + comments : /@license|@preserve|^!/ + } }, admin: { files: [{ @@ -54,21 +56,38 @@ module.exports = function( grunt ){ cwd: '<%= dirs.js %>/admin/', src: [ '*.js', - '!*.min.js', - '!Gruntfile.js', - '!jquery.flot*' // !jquery.flot* prevents to join all jquery.flot files in jquery.min.js + '!*.min.js' ], dest: '<%= dirs.js %>/admin/', ext: '.min.js' }] }, - adminflot: { // minify correctly the jquery.flot lib + vendor: { files: { - '<%= dirs.js %>/admin/jquery.flot.min.js': ['<%= dirs.js %>/admin/jquery.flot.js'], - '<%= dirs.js %>/admin/jquery.flot.pie.min.js': ['<%= dirs.js %>/admin/jquery.flot.pie.js'], - '<%= dirs.js %>/admin/jquery.flot.resize.min.js': ['<%= dirs.js %>/admin/jquery.flot.resize.js'], - '<%= dirs.js %>/admin/jquery.flot.stack.min.js': ['<%= dirs.js %>/admin/jquery.flot.stack.js'], - '<%= dirs.js %>/admin/jquery.flot.time.min.js': ['<%= dirs.js %>/admin/jquery.flot.time.js'], + '<%= dirs.js %>/accounting/accounting.min.js': ['<%= dirs.js %>/accounting/accounting.js'], + '<%= dirs.js %>/jquery-blockui/jquery.blockUI.min.js': ['<%= dirs.js %>/jquery-blockui/jquery.blockUI.js'], + '<%= dirs.js %>/jquery-cookie/jquery.cookie.min.js': ['<%= dirs.js %>/jquery-cookie/jquery.cookie.js'], + '<%= dirs.js %>/js-cookie/js.cookie.min.js': ['<%= dirs.js %>/js-cookie/js.cookie.js'], + '<%= dirs.js %>/jquery-flot/jquery.flot.min.js': ['<%= dirs.js %>/jquery-flot/jquery.flot.js'], + '<%= dirs.js %>/jquery-flot/jquery.flot.pie.min.js': ['<%= dirs.js %>/jquery-flot/jquery.flot.pie.js'], + '<%= dirs.js %>/jquery-flot/jquery.flot.resize.min.js': ['<%= dirs.js %>/jquery-flot/jquery.flot.resize.js'], + '<%= dirs.js %>/jquery-flot/jquery.flot.stack.min.js': ['<%= dirs.js %>/jquery-flot/jquery.flot.stack.js'], + '<%= dirs.js %>/jquery-flot/jquery.flot.time.min.js': ['<%= dirs.js %>/jquery-flot/jquery.flot.time.js'], + '<%= dirs.js %>/jquery-payment/jquery.payment.min.js': ['<%= dirs.js %>/jquery-payment/jquery.payment.js'], + '<%= dirs.js %>/jquery-qrcode/jquery.qrcode.min.js': ['<%= dirs.js %>/jquery-qrcode/jquery.qrcode.js'], + '<%= dirs.js %>/jquery-serializejson/jquery.serializejson.min.js': ['<%= dirs.js %>/jquery-serializejson/jquery.serializejson.js'], + '<%= dirs.js %>/jquery-tiptip/jquery.tipTip.min.js': ['<%= dirs.js %>/jquery-tiptip/jquery.tipTip.js'], + '<%= dirs.js %>/jquery-ui-touch-punch/jquery-ui-touch-punch.min.js': ['<%= dirs.js %>/jquery-ui-touch-punch/jquery-ui-touch-punch.js'], + '<%= dirs.js %>/prettyPhoto/jquery.prettyPhoto.init.min.js': ['<%= dirs.js %>/prettyPhoto/jquery.prettyPhoto.init.js'], + '<%= dirs.js %>/prettyPhoto/jquery.prettyPhoto.min.js': ['<%= dirs.js %>/prettyPhoto/jquery.prettyPhoto.js'], + '<%= dirs.js %>/flexslider/jquery.flexslider.min.js': ['<%= dirs.js %>/flexslider/jquery.flexslider.js'], + '<%= dirs.js %>/zoom/jquery.zoom.min.js': ['<%= dirs.js %>/zoom/jquery.zoom.js'], + '<%= dirs.js %>/photoswipe/photoswipe.min.js': ['<%= dirs.js %>/photoswipe/photoswipe.js'], + '<%= dirs.js %>/photoswipe/photoswipe-ui-default.min.js': ['<%= dirs.js %>/photoswipe/photoswipe-ui-default.js'], + '<%= dirs.js %>/round/round.min.js': ['<%= dirs.js %>/round/round.js'], + '<%= dirs.js %>/select2/select2.min.js': ['<%= dirs.js %>/select2/select2.js'], + '<%= dirs.js %>/stupidtable/stupidtable.min.js': ['<%= dirs.js %>/stupidtable/stupidtable.js'], + '<%= dirs.js %>/zeroclipboard/jquery.zeroclipboard.min.js': ['<%= dirs.js %>/zeroclipboard/jquery.zeroclipboard.js'] } }, frontend: { @@ -83,109 +102,267 @@ module.exports = function( grunt ){ ext: '.min.js' }] }, + simplify_commerce: { + files: [{ + expand: true, + cwd: 'includes/gateways/simplify-commerce/assets/js/', + src: [ + '*.js', + '!*.min.js' + ], + dest: 'includes/gateways/simplify-commerce/assets/js/', + ext: '.min.js' + }] + } }, - // Watch changes for assets + // Compile all .scss files. + sass: { + compile: { + options: { + sourceMap: 'none' + }, + files: [{ + expand: true, + cwd: '<%= dirs.css %>/', + src: ['*.scss'], + dest: '<%= dirs.css %>/', + ext: '.css' + }] + } + }, + + // Generate RTL .css files + rtlcss: { + woocommerce: { + expand: true, + cwd: '<%= dirs.css %>', + src: [ + '*.css', + '!select2.css', + '!*-rtl.css' + ], + dest: '<%= dirs.css %>/', + ext: '-rtl.css' + } + }, + + // Minify all .css files. + cssmin: { + minify: { + expand: true, + cwd: '<%= dirs.css %>/', + src: ['*.css'], + dest: '<%= dirs.css %>/', + ext: '.css' + } + }, + + // Concatenate select2.css onto the admin.css files. + concat: { + admin: { + files: { + '<%= dirs.css %>/admin.css' : ['<%= dirs.css %>/select2.css', '<%= dirs.css %>/admin.css'], + '<%= dirs.css %>/admin-rtl.css' : ['<%= dirs.css %>/select2.css', '<%= dirs.css %>/admin-rtl.css'] + } + } + }, + + // Watch changes for assets. watch: { - less: { - files: ['<%= dirs.css %>/*.less'], - tasks: ['less', 'cssmin'], + css: { + files: ['<%= dirs.css %>/*.scss'], + tasks: ['sass', 'rtlcss', 'cssmin', 'concat'] }, js: { files: [ '<%= dirs.js %>/admin/*js', '<%= dirs.js %>/frontend/*js', '!<%= dirs.js %>/admin/*.min.js', - '!<%= dirs.js %>/frontend/*.min.js', + '!<%= dirs.js %>/frontend/*.min.js' ], - tasks: ['uglify'] + tasks: ['jshint', 'uglify'] } }, + // Generate POT files. + makepot: { + options: { + type: 'wp-plugin', + domainPath: 'i18n/languages', + potHeaders: { + 'report-msgid-bugs-to': 'https://github.com/woocommerce/woocommerce/issues', + 'language-team': 'LANGUAGE ' + } + }, + dist: { + options: { + potFilename: 'woocommerce.pot', + exclude: [ + 'apigen/.*', + 'tests/.*', + 'tmp/.*' + ] + } + } + }, + + // Check textdomain errors. + checktextdomain: { + options:{ + text_domain: 'woocommerce', + keywords: [ + '__:1,2d', + '_e:1,2d', + '_x:1,2c,3d', + 'esc_html__:1,2d', + 'esc_html_e:1,2d', + 'esc_html_x:1,2c,3d', + 'esc_attr__:1,2d', + 'esc_attr_e:1,2d', + 'esc_attr_x:1,2c,3d', + '_ex:1,2c,3d', + '_n:1,2,4d', + '_nx:1,2,4c,5d', + '_n_noop:1,2,3d', + '_nx_noop:1,2,3c,4d' + ] + }, + files: { + src: [ + '**/*.php', // Include all files + '!apigen/**', // Exclude apigen/ + '!node_modules/**', // Exclude node_modules/ + '!tests/**', // Exclude tests/ + '!vendor/**', // Exclude vendor/ + '!tmp/**' // Exclude tmp/ + ], + expand: true + } + }, + + // Exec shell commands. shell: { options: { stdout: true, stderr: true }, - generatepot: { - command: [ - 'cd i18n/makepot/', - 'sed -i "" "s/exit( \'Locked\' );/\\/\\/exit( \'Locked\' );/g" index.php', - 'php index.php generate', - 'sed -i "" "s/\\/\\/exit( \'Locked\' );/exit( \'Locked\' );/g" index.php', - ].join( '&&' ) - }, apigen: { command: [ - 'cd apigen/', - 'php apigen.php --source ../ --destination ../wc-apidocs --download yes --template-config ./templates/woodocs/config.neon --title "WooCommerce" --exclude "*/mijireh/*" --exclude "*/includes/libraries/*" --exclude "*/i18n/*" --exclude "*/node_modules/*" --exclude "*/deploy/*" --exclude "*/apigen/*" --exclude "*/wc-apidocs/*"', + 'apigen generate -q', + 'cd apigen', + 'php hook-docs.php' ].join( '&&' ) + }, + e2e_test: { + command: 'npm run --silent test:single tests/e2e-tests/' + grunt.option( 'file' ) + }, + e2e_tests: { + command: 'npm run --silent test' } }, - copy: { - deploy: { - src: [ - '**', - '!.*', - '!.*/**', - '.htaccess', - '!Gruntfile.js', - '!sftp-config.json', - '!package.json', - '!node_modules/**', - '!wc-apidocs/**', - '!apigen/**' - ], - dest: 'deploy', - expand: true, - dot: true - }, - }, - + // Clean the directory. clean: { apigen: { src: [ 'wc-apidocs' ] + } + }, + + // PHP Code Sniffer. + phpcs: { + options: { + bin: 'vendor/bin/phpcs', + standard: './phpcs.ruleset.xml' }, - deploy: { - src: [ 'deploy' ] + dist: { + src: [ + '**/*.php', // Include all files + '!apigen/**', // Exclude apigen/ + '!includes/api/legacy/**', // Exclude legacy REST API + '!includes/gateways/simplify-commerce/includes/Simplify/**', // Exclude simplify commerce SDK + '!includes/libraries/**', // Exclude libraries/ + '!node_modules/**', // Exclude node_modules/ + '!tests/cli/**', // Exclude tests/cli/ + '!tmp/**', // Exclude tmp/ + '!vendor/**' // Exclude vendor/ + ] + } + }, + + // Autoprefixer. + postcss: { + options: { + processors: [ + require( 'autoprefixer' )({ + browsers: [ + '> 0.1%', + 'ie 8', + 'ie 9' + ] + }) + ] }, + dist: { + src: [ + '<%= dirs.css %>/*.css' + ] + } } }); // Load NPM tasks to be used here + grunt.loadNpmTasks( 'grunt-sass' ); grunt.loadNpmTasks( 'grunt-shell' ); - grunt.loadNpmTasks( 'grunt-contrib-less' ); - grunt.loadNpmTasks( 'grunt-contrib-cssmin' ); + grunt.loadNpmTasks( 'grunt-phpcs' ); + grunt.loadNpmTasks( 'grunt-rtlcss' ); + grunt.loadNpmTasks( 'grunt-postcss' ); + grunt.loadNpmTasks( 'grunt-stylelint' ); + grunt.loadNpmTasks( 'grunt-wp-i18n' ); + grunt.loadNpmTasks( 'grunt-checktextdomain' ); + grunt.loadNpmTasks( 'grunt-contrib-jshint' ); grunt.loadNpmTasks( 'grunt-contrib-uglify' ); + grunt.loadNpmTasks( 'grunt-contrib-cssmin' ); + grunt.loadNpmTasks( 'grunt-contrib-concat' ); grunt.loadNpmTasks( 'grunt-contrib-watch' ); - grunt.loadNpmTasks( 'grunt-contrib-copy' ); grunt.loadNpmTasks( 'grunt-contrib-clean' ); // Register tasks grunt.registerTask( 'default', [ - 'less', - 'cssmin', - 'uglify' + 'jshint', + 'uglify', + 'css' ]); - // Just an alias for pot file generation - grunt.registerTask( 'pot', [ - 'shell:generatepot' + grunt.registerTask( 'js', [ + 'jshint', + 'uglify:admin', + 'uglify:frontend' + ]); + + grunt.registerTask( 'css', [ + 'sass', + 'rtlcss', + 'postcss', + 'cssmin', + 'concat' ]); grunt.registerTask( 'docs', [ - 'clean:apigen', + 'clean:apigen', 'shell:apigen' ]); grunt.registerTask( 'dev', [ 'default', - 'pot' + 'makepot' ]); - grunt.registerTask( 'deploy', [ - 'clean:deploy', - 'copy:deploy' + grunt.registerTask( 'e2e-tests', [ + 'shell:e2e_tests' ]); -}; \ No newline at end of file + + grunt.registerTask( 'e2e-test', [ + 'shell:e2e_test' + ]); +}; diff --git a/README.md b/README.md index f56f69cd9ab..0cfb6af8883 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,27 @@ -# WooCommerce - excelling eCommerce -[![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) +# [WooCommerce](https://woocommerce.com/) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) [![Build Status](https://travis-ci.org/woocommerce/woocommerce.svg?branch=master)](https://travis-ci.org/woocommerce/woocommerce) [![Code Coverage](https://scrutinizer-ci.com/g/woocommerce/woocommerce/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/woocommerce/woocommerce/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/woocommerce/woocommerce/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/woocommerce/woocommerce/?branch=master) [![Code Climate](https://codeclimate.com/github/woocommerce/woocommerce/badges/gpa.svg)](https://codeclimate.com/github/woocommerce/woocommerce) -Welcome to the WooCommerce repository on GitHub. Here you can browse the source, look at open issues and keep track of development. We recommend all developers to follow the [WooCommerce development blog](http://develop.woothemes.com/woocommerce/) to stay up to date about everything happening in the project. You can also [follow @DevelopWC](https://twitter.com/DevelopWC) on Twitter for the latest development updates. +Welcome to the WooCommerce repository on GitHub. Here you can browse the source, look at open issues and keep track of development. We recommend all developers to follow the [WooCommerce development blog](https://woocommerce.wordpress.com/) to stay up to date about everything happening in the project. You can also [follow @DevelopWC](https://twitter.com/DevelopWC) on Twitter for the latest development updates. -If you are not a developer, please use the [WooCommerce plugin page](http://wordpress.org/plugins/woocommerce/) on WordPress.org. +If you are not a developer, please use the [WooCommerce plugin page](https://wordpress.org/plugins/woocommerce/) on WordPress.org. + +## Documentation +* [WooCommerce Documentation](https://docs.woocommerce.com/documentation/plugins/woocommerce/) +* [WooCommerce Code Reference](https://docs.woocommerce.com/wc-apidocs/) +* [WooCommerce REST API Docs](https://woocommerce.github.io/woocommerce-rest-api-docs/) + +## Reporting Security Issues +To disclose a security issue to our team, [please submit a report via HackerOne here](https://hackerone.com/automattic/). ## Support -This repository is not suitable for support. Please don't use our issue tracker for support requests, but for core WooCommerce issues only. Support can take place in the appropriate channels: +This repository is not suitable for support. Please don't use our issue tracker for support requests, but for core, WooCommerce issues only. Support can take place through the appropriate channels: -* The [public support forums](http://wordpress.org/support/plugin/woocommerce) on WordPress.org, where the community can help each other out. -* The [WooThemes premium support portal](http://support.woothemes.com/) for customers who have purchased themes or extensions. +* The [WooCommerce premium support portal](https://woocommerce.com/my-account/create-a-ticket/) for customers who have purchased themes or extensions. +* [Our community forum on wp.org](https://wordpress.org/support/plugin/woocommerce) which is available for all WooCommerce users. Support requests in issues on this repository will be closed on sight. - ## Contributing to WooCommerce -If you have a patch, or stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woothemes/woocommerce/blob/master/CONTRIBUTING.md) for more information how you can do this. \ No newline at end of file +If you have a patch or have stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woocommerce/woocommerce/blob/master/.github/CONTRIBUTING.md) for more information how you can do this. + +## Contributing new features to the WooCommerce REST API +If you're like to add a feature to the next version of the REST API, contribute here: https://github.com/woocommerce/wc-api-dev diff --git a/apigen.neon b/apigen.neon new file mode 100644 index 00000000000..d8a09352fd7 --- /dev/null +++ b/apigen.neon @@ -0,0 +1,53 @@ +source: ./ + +destination: wc-apidocs + +templateConfig: apigen/theme-woocommerce/config.neon + +# list of scanned file extensions (e.g. php5, phpt...) +extensions: [php] + +# directories and files matching this file mask will not be parsed +exclude: + - includes/libraries/ + - includes/api/legacy/ + - i18n/ + - node_modules/ + - wc-apidocs/ + - tmp/ + - tests/ + - .sass-cache/ + - apigen/ + +# character set of source files; if you use only one across your files, we recommend you name it +charset: [UTF-8] + +# elements with this name prefix will be considered as the "main project" (the rest will be considered as libraries) +main: WC + +# title of generated documentation +title: WooCommerce 3.0.x Code Reference + +# base url used for sitemap (useful for public doc) +baseUrl: https://docs.woocommerce.com/wc-apidocs/ + +# choose ApiGen template theme +templateTheme: default + +# generate documentation for PHP internal classes +php: false + +# generate highlighted source code for elements +sourceCode: true + +# generate tree view of classes, interfaces, traits and exceptions +tree: true + +# generate documentation for deprecated elements +deprecated: true + +# generate list of tasks with @ todo annotation +todo: true + +# add link to ZIP archive of documentation +download: false diff --git a/apigen/ApiGen/Backend.php b/apigen/ApiGen/Backend.php deleted file mode 100644 index 4f201387d1d..00000000000 --- a/apigen/ApiGen/Backend.php +++ /dev/null @@ -1,301 +0,0 @@ -generator = $generator; - $this->cacheTokenStreams = $cacheTokenStreams; - } - - /** - * Destructor. - * - * Deletes all cached token streams. - */ - public function __destruct() - { - foreach ($this->fileCache as $file) { - unlink($file); - } - } - - /** - * Adds a file to the backend storage. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token stream - * @param \TokenReflection\ReflectionFile $file File reflection object - * @return \TokenReflection\Broker\Backend\Memory - */ - public function addFile(TokenReflection\Stream\StreamBase $tokenStream, TokenReflection\ReflectionFile $file) - { - if ($this->cacheTokenStreams) { - $this->fileCache[$file->getName()] = $cacheFile = tempnam(sys_get_temp_dir(), 'trc'); - file_put_contents($cacheFile, serialize($tokenStream)); - } - - parent::addFile($tokenStream, $file); - - return $this; - } - - /** - * Returns an array of tokens for a particular file. - * - * @param string $fileName File name - * @return \TokenReflection\Stream - * @throws \RuntimeException If the token stream could not be returned. - */ - public function getFileTokens($fileName) - { - try { - if (!$this->isFileProcessed($fileName)) { - throw new InvalidArgumentException('File was not processed'); - } - - $realName = Broker::getRealPath($fileName); - if (!isset($this->fileCache[$realName])) { - throw new InvalidArgumentException('File is not in the cache'); - } - - $data = @file_get_contents($this->fileCache[$realName]); - if (false === $data) { - throw new RuntimeException('Cached file is not readable'); - } - $file = @unserialize($data); - if (false === $file) { - throw new RuntimeException('Stream could not be loaded from cache'); - } - - return $file; - } catch (\Exception $e) { - throw new RuntimeException(sprintf('Could not return token stream for file %s', $fileName), 0, $e); - } - } - - /** - * Prepares and returns used class lists. - * - * @return array - */ - protected function parseClassLists() - { - $allClasses = array( - self::TOKENIZED_CLASSES => array(), - self::INTERNAL_CLASSES => array(), - self::NONEXISTENT_CLASSES => array() - ); - - $declared = array_flip(array_merge(get_declared_classes(), get_declared_interfaces())); - - foreach ($this->getNamespaces() as $namespace) { - foreach ($namespace->getClasses() as $name => $trClass) { - $class = new ReflectionClass($trClass, $this->generator); - $allClasses[self::TOKENIZED_CLASSES][$name] = $class; - if (!$class->isDocumented()) { - continue; - } - - foreach (array_merge($trClass->getParentClasses(), $trClass->getInterfaces()) as $parentName => $parent) { - if ($parent->isInternal()) { - if (!isset($allClasses[self::INTERNAL_CLASSES][$parentName])) { - $allClasses[self::INTERNAL_CLASSES][$parentName] = $parent; - } - } elseif (!$parent->isTokenized()) { - if (!isset($allClasses[self::NONEXISTENT_CLASSES][$parentName])) { - $allClasses[self::NONEXISTENT_CLASSES][$parentName] = $parent; - } - } - } - - $this->generator->checkMemory(); - } - } - - foreach ($allClasses[self::TOKENIZED_CLASSES] as $class) { - if (!$class->isDocumented()) { - continue; - } - - foreach ($class->getOwnMethods() as $method) { - $allClasses = $this->processFunction($declared, $allClasses, $method); - } - - foreach ($class->getOwnProperties() as $property) { - $annotations = $property->getAnnotations(); - - if (!isset($annotations['var'])) { - continue; - } - - foreach ($annotations['var'] as $doc) { - foreach (explode('|', preg_replace('~\\s.*~', '', $doc)) as $name) { - if ($name = rtrim($name, '[]')) { - $name = Resolver::resolveClassFQN($name, $class->getNamespaceAliases(), $class->getNamespaceName()); - $allClasses = $this->addClass($declared, $allClasses, $name); - } - } - } - } - - $this->generator->checkMemory(); - } - - foreach ($this->getFunctions() as $function) { - $allClasses = $this->processFunction($declared, $allClasses, $function); - } - - array_walk_recursive($allClasses, function(&$reflection, $name, Generator $generator) { - if (!$reflection instanceof ReflectionClass) { - $reflection = new ReflectionClass($reflection, $generator); - } - }, $this->generator); - - return $allClasses; - } - - /** - * Processes a function/method and adds classes from annotations to the overall class array. - * - * @param array $declared Array of declared classes - * @param array $allClasses Array with all classes parsed so far - * @param \ApiGen\ReflectionFunction|\TokenReflection\IReflectionFunctionBase $function Function/method reflection - * @return array - */ - private function processFunction(array $declared, array $allClasses, $function) - { - static $parsedAnnotations = array('param', 'return', 'throws'); - - $annotations = $function->getAnnotations(); - foreach ($parsedAnnotations as $annotation) { - if (!isset($annotations[$annotation])) { - continue; - } - - foreach ($annotations[$annotation] as $doc) { - foreach (explode('|', preg_replace('~\\s.*~', '', $doc)) as $name) { - if ($name) { - $name = Resolver::resolveClassFQN(rtrim($name, '[]'), $function->getNamespaceAliases(), $function->getNamespaceName()); - $allClasses = $this->addClass($declared, $allClasses, $name); - } - } - } - } - - foreach ($function->getParameters() as $param) { - if ($hint = $param->getClassName()) { - $allClasses = $this->addClass($declared, $allClasses, $hint); - } - } - - return $allClasses; - } - - /** - * Adds a class to list of classes. - * - * @param array $declared Array of declared classes - * @param array $allClasses Array with all classes parsed so far - * @param string $name Class name - * @return array - */ - private function addClass(array $declared, array $allClasses, $name) - { - $name = ltrim($name, '\\'); - - if (!isset($declared[$name]) || isset($allClasses[self::TOKENIZED_CLASSES][$name]) - || isset($allClasses[self::INTERNAL_CLASSES][$name]) || isset($allClasses[self::NONEXISTENT_CLASSES][$name]) - ) { - return $allClasses; - } - - $parameterClass = $this->getBroker()->getClass($name); - if ($parameterClass->isInternal()) { - $allClasses[self::INTERNAL_CLASSES][$name] = $parameterClass; - foreach (array_merge($parameterClass->getInterfaces(), $parameterClass->getParentClasses()) as $parentClass) { - if (!isset($allClasses[self::INTERNAL_CLASSES][$parentName = $parentClass->getName()])) { - $allClasses[self::INTERNAL_CLASSES][$parentName] = $parentClass; - } - } - } elseif (!$parameterClass->isTokenized() && !isset($allClasses[self::NONEXISTENT_CLASSES][$name])) { - $allClasses[self::NONEXISTENT_CLASSES][$name] = $parameterClass; - } - - return $allClasses; - } - - /** - * Returns all constants from all namespaces. - * - * @return array - */ - public function getConstants() - { - $generator = $this->generator; - return array_map(function(IReflectionConstant $constant) use ($generator) { - return new ReflectionConstant($constant, $generator); - }, parent::getConstants()); - } - - /** - * Returns all functions from all namespaces. - * - * @return array - */ - public function getFunctions() - { - $generator = $this->generator; - return array_map(function(IReflectionFunction $function) use ($generator) { - return new ReflectionFunction($function, $generator); - }, parent::getFunctions()); - } -} diff --git a/apigen/ApiGen/Config.php b/apigen/ApiGen/Config.php deleted file mode 100644 index ea35741dc7c..00000000000 --- a/apigen/ApiGen/Config.php +++ /dev/null @@ -1,602 +0,0 @@ - '', - 'source' => array(), - 'destination' => '', - 'extensions' => array('php'), - 'exclude' => array(), - 'skipDocPath' => array(), - 'skipDocPrefix' => array(), - 'charset' => array('auto'), - 'main' => '', - 'title' => '', - 'baseUrl' => '', - 'googleCseId' => '', - 'googleCseLabel' => '', - 'googleAnalytics' => '', - 'templateConfig' => '', - 'allowedHtml' => array('b', 'i', 'a', 'ul', 'ol', 'li', 'p', 'br', 'var', 'samp', 'kbd', 'tt'), - 'groups' => 'auto', - 'autocomplete' => array('classes', 'constants', 'functions'), - 'accessLevels' => array('public', 'protected'), - 'internal' => false, - 'php' => true, - 'tree' => true, - 'deprecated' => false, - 'todo' => false, - 'download' => false, - 'sourceCode' => true, - 'report' => '', - 'undocumented' => '', - 'wipeout' => true, - 'quiet' => false, - 'progressbar' => true, - 'colors' => true, - 'updateCheck' => true, - 'debug' => false - ); - - /** - * File or directory path options. - * - * @var array - */ - private static $pathOptions = array( - 'config', - 'source', - 'destination', - 'templateConfig', - 'report' - ); - - /** - * Possible values for options. - * - * @var array - */ - private static $possibleOptionsValues = array( - 'groups' => array('auto', 'namespaces', 'packages', 'none'), - 'autocomplete' => array('classes', 'constants', 'functions', 'methods', 'properties', 'classconstants'), - 'accessLevels' => array('public', 'protected', 'private') - ); - - /** - * Initializes default configuration. - */ - public function __construct() - { - $templateDir = self::isInstalledByPear() ? '@data_dir@' . DIRECTORY_SEPARATOR . 'ApiGen' : realpath(__DIR__ . DIRECTORY_SEPARATOR . '..'); - self::$defaultConfig['templateConfig'] = $templateDir . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . 'default' . DIRECTORY_SEPARATOR . 'config.neon'; - self::$defaultConfig['colors'] = 'WIN' !== substr(PHP_OS, 0, 3); - $this->config = self::$defaultConfig; - } - - /** - * Processes command line options. - * - * @param array $options - * @return \ApiGen\Config - */ - public function processCliOptions(array $options) - { - while ($option = current($options)) { - if (preg_match('~^--([a-z][-a-z]*[a-z])(?:=(.+))?$~', $option, $matches) || preg_match('~^-([a-z])=?(.*)~', $option, $matches)) { - $name = $matches[1]; - - if (!empty($matches[2])) { - $value = $matches[2]; - } else { - $next = next($options); - if (false === $next || '-' === $next{0}) { - prev($options); - $value = ''; - } else { - $value = $next; - } - } - - $this->options[$name][] = $value; - } - - next($options); - } - $this->options = array_map(function($value) { - return 1 === count($value) ? $value[0] : $value; - }, $this->options); - - // Compatibility with ApiGen 1.0 - foreach (array('config', 'source', 'destination') as $option) { - if (isset($this->options[$option{0}]) && !isset($this->options[$option])) { - $this->options[$option] = $this->options[$option{0}]; - } - unset($this->options[$option{0}]); - } - - return $this; - } - - /** - * Prepares configuration. - * - * @return \ApiGen\Config - * @throws \ApiGen\ConfigException If something in configuration is wrong. - */ - public function prepare() - { - // Command line options - $cli = array(); - $translator = array(); - foreach ($this->options as $option => $value) { - $converted = preg_replace_callback('~-([a-z])~', function($matches) { - return strtoupper($matches[1]); - }, $option); - - $cli[$converted] = $value; - $translator[$converted] = $option; - } - - $unknownOptions = array_keys(array_diff_key($cli, self::$defaultConfig)); - if (!empty($unknownOptions)) { - $originalOptions = array_map(function($option) { - return (1 === strlen($option) ? '-' : '--') . $option; - }, array_values(array_diff_key($translator, self::$defaultConfig))); - - $message = count($unknownOptions) > 1 - ? sprintf('Unknown command line options "%s"', implode('", "', $originalOptions)) - : sprintf('Unknown command line option "%s"', $originalOptions[0]); - throw new ConfigException($message); - } - - // Config file - $neon = array(); - if (empty($this->options) && $this->defaultConfigExists()) { - $this->options['config'] = $this->getDefaultConfigPath(); - } - if (isset($this->options['config']) && is_file($this->options['config'])) { - $neon = Neon::decode(file_get_contents($this->options['config'])); - foreach (self::$pathOptions as $option) { - if (!empty($neon[$option])) { - if (is_array($neon[$option])) { - foreach ($neon[$option] as $key => $value) { - $neon[$option][$key] = $this->getAbsolutePath($value); - } - } else { - $neon[$option] = $this->getAbsolutePath($neon[$option]); - } - } - } - - $unknownOptions = array_keys(array_diff_key($neon, self::$defaultConfig)); - if (!empty($unknownOptions)) { - $message = count($unknownOptions) > 1 - ? sprintf('Unknown config file options "%s"', implode('", "', $unknownOptions)) - : sprintf('Unknown config file option "%s"', $unknownOptions[0]); - throw new ConfigException($message); - } - } - - // Merge options - $this->config = array_merge(self::$defaultConfig, $neon, $cli); - - // Compatibility with old option name "undocumented" - if (!isset($this->config['report']) && isset($this->config['undocumented'])) { - $this->config['report'] = $this->config['undocumented']; - unset($this->config['undocumented']); - } - - foreach (self::$defaultConfig as $option => $valueDefinition) { - if (is_array($this->config[$option]) && !is_array($valueDefinition)) { - throw new ConfigException(sprintf('Option "%s" must be set only once', $option)); - } - - if (is_bool($this->config[$option]) && !is_bool($valueDefinition)) { - throw new ConfigException(sprintf('Option "%s" expects value', $option)); - } - - if (is_bool($valueDefinition) && !is_bool($this->config[$option])) { - // Boolean option - $value = strtolower($this->config[$option]); - if ('on' === $value || 'yes' === $value || 'true' === $value || '' === $value) { - $value = true; - } elseif ('off' === $value || 'no' === $value || 'false' === $value) { - $value = false; - } - $this->config[$option] = (bool) $value; - } elseif (is_array($valueDefinition)) { - // Array option - $this->config[$option] = array_unique((array) $this->config[$option]); - foreach ($this->config[$option] as $key => $value) { - $value = explode(',', $value); - while (count($value) > 1) { - array_push($this->config[$option], array_shift($value)); - } - $this->config[$option][$key] = array_shift($value); - } - $this->config[$option] = array_filter($this->config[$option]); - } - - // Check posssible values - if (!empty(self::$possibleOptionsValues[$option])) { - $values = self::$possibleOptionsValues[$option]; - - if (is_array($valueDefinition)) { - $this->config[$option] = array_filter($this->config[$option], function($value) use ($values) { - return in_array($value, $values); - }); - } elseif (!in_array($this->config[$option], $values)) { - $this->config[$option] = ''; - } - } - } - - // Unify character sets - $this->config['charset'] = array_map('strtoupper', $this->config['charset']); - - // Process options that specify a filesystem path - foreach (self::$pathOptions as $option) { - if (is_array($this->config[$option])) { - array_walk($this->config[$option], function(&$value) { - if (file_exists($value)) { - $value = realpath($value); - } - }); - usort($this->config[$option], 'strcasecmp'); - } else { - if (file_exists($this->config[$option])) { - $this->config[$option] = realpath($this->config[$option]); - } - } - } - - // Unify directory separators - foreach (array('exclude', 'skipDocPath') as $option) { - $this->config[$option] = array_map(function($mask) { - return str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $mask); - }, $this->config[$option]); - usort($this->config[$option], 'strcasecmp'); - } - - // Unify prefixes - $this->config['skipDocPrefix'] = array_map(function($prefix) { - return ltrim($prefix, '\\'); - }, $this->config['skipDocPrefix']); - usort($this->config['skipDocPrefix'], 'strcasecmp'); - - // Base url without slash at the end - $this->config['baseUrl'] = rtrim($this->config['baseUrl'], '/'); - - // No progressbar in quiet mode - if ($this->config['quiet']) { - $this->config['progressbar'] = false; - } - - // Check - $this->check(); - - // Default template config - $this->config['template'] = array( - 'require' => array(), - 'resources' => array(), - 'templates' => array( - 'common' => array(), - 'optional' => array() - ) - ); - - // Merge template config - $this->config = array_merge_recursive($this->config, array('template' => Neon::decode(file_get_contents($fileName = $this->config['templateConfig'])))); - $this->config['template']['config'] = realpath($fileName); - - // Check template - $this->checkTemplate(); - - return $this; - } - - /** - * Checks configuration. - * - * @return \ApiGen\Config - * @throws \ApiGen\ConfigException If something in configuration is wrong. - */ - private function check() - { - if (!empty($this->config['config']) && !is_file($this->config['config'])) { - throw new ConfigException(sprintf('Config file "%s" doesn\'t exist', $this->config['config'])); - } - - if (empty($this->config['source'])) { - throw new ConfigException('Source is not set'); - } - foreach ($this->config['source'] as $source) { - if (!file_exists($source)) { - throw new ConfigException(sprintf('Source "%s" doesn\'t exist', $source)); - } - } - - if (empty($this->config['destination'])) { - throw new ConfigException('Destination is not set'); - } - - foreach ($this->config['extensions'] as $extension) { - if (!preg_match('~^[a-z\\d]+$~i', $extension)) { - throw new ConfigException(sprintf('Invalid file extension "%s"', $extension)); - } - } - - if (!is_file($this->config['templateConfig'])) { - throw new ConfigException(sprintf('Template config "%s" doesn\'t exist', $this->config['templateConfig'])); - } - - if (!empty($this->config['baseUrl']) && !preg_match('~^https?://(?:[-a-z0-9]+\.)+[a-z]{2,6}(?:/.*)?$~i', $this->config['baseUrl'])) { - throw new ConfigException(sprintf('Invalid base url "%s"', $this->config['baseUrl'])); - } - - if (!empty($this->config['googleCseId']) && !preg_match('~^\d{21}:[-a-z0-9_]{11}$~', $this->config['googleCseId'])) { - throw new ConfigException(sprintf('Invalid Google Custom Search ID "%s"', $this->config['googleCseId'])); - } - - if (!empty($this->config['googleAnalytics']) && !preg_match('~^UA\\-\\d+\\-\\d+$~', $this->config['googleAnalytics'])) { - throw new ConfigException(sprintf('Invalid Google Analytics tracking code "%s"', $this->config['googleAnalytics'])); - } - - if (empty($this->config['groups'])) { - throw new ConfigException('No supported groups value given'); - } - - if (empty($this->config['autocomplete'])) { - throw new ConfigException('No supported autocomplete value given'); - } - - if (empty($this->config['accessLevels'])) { - throw new ConfigException('No supported access level given'); - } - - return $this; - } - - /** - * Checks template configuration. - * - * @return \ApiGen\Config - * @throws \ApiGen\ConfigException If something in template configuration is wrong. - */ - private function checkTemplate() - { - $require = $this->config['template']['require']; - if (isset($require['min']) && !preg_match('~^\\d+(?:\\.\\d+){0,2}$~', $require['min'])) { - throw new ConfigException(sprintf('Invalid minimal version definition "%s"', $require['min'])); - } - if (isset($require['max']) && !preg_match('~^\\d+(?:\\.\\d+){0,2}$~', $require['max'])) { - throw new ConfigException(sprintf('Invalid maximal version definition "%s"', $require['max'])); - } - - $isMinOk = function($min) { - $min .= str_repeat('.0', 2 - substr_count($min, '.')); - return version_compare($min, Generator::VERSION, '<='); - }; - $isMaxOk = function($max) { - $max .= str_repeat('.0', 2 - substr_count($max, '.')); - return version_compare($max, Generator::VERSION, '>='); - }; - - if (isset($require['min'], $require['max']) && (!$isMinOk($require['min']) || !$isMaxOk($require['max']))) { - throw new ConfigException(sprintf('The template requires version from "%s" to "%s", you are using version "%s"', $require['min'], $require['max'], Generator::VERSION)); - } elseif (isset($require['min']) && !$isMinOk($require['min'])) { - throw new ConfigException(sprintf('The template requires version "%s" or newer, you are using version "%s"', $require['min'], Generator::VERSION)); - } elseif (isset($require['max']) && !$isMaxOk($require['max'])) { - throw new ConfigException(sprintf('The template requires version "%s" or older, you are using version "%s"', $require['max'], Generator::VERSION)); - } - - foreach (array('main', 'optional') as $section) { - foreach ($this->config['template']['templates'][$section] as $type => $config) { - if (!isset($config['filename'])) { - throw new ConfigException(sprintf('Filename for "%s" is not defined', $type)); - } - if (!isset($config['template'])) { - throw new ConfigException(sprintf('Template for "%s" is not defined', $type)); - } - if (!is_file(dirname($this->config['templateConfig']) . DIRECTORY_SEPARATOR . $config['template'])) { - throw new ConfigException(sprintf('Template for "%s" doesn\'t exist', $type)); - } - } - } - - return $this; - } - - /** - * Returns default configuration file path. - * - * @return string - */ - private function getDefaultConfigPath() - { - return getcwd() . DIRECTORY_SEPARATOR . 'apigen.neon'; - } - - /** - * Checks if default configuration file exists. - * - * @return boolean - */ - private function defaultConfigExists() - { - return is_file($this->getDefaultConfigPath()); - } - - /** - * Returns absolute path. - * - * @param string $path Path - * @return string - */ - private function getAbsolutePath($path) - { - if (preg_match('~/|[a-z]:~Ai', $path)) { - return $path; - } - - return dirname($this->options['config']) . DIRECTORY_SEPARATOR . $path; - } - - /** - * Checks if a configuration option exists. - * - * @param string $name Option name - * @return boolean - */ - public function __isset($name) - { - return isset($this->config[$name]); - } - - /** - * Returns a configuration option value. - * - * @param string $name Option name - * @return mixed - */ - public function __get($name) - { - return isset($this->config[$name]) ? $this->config[$name] : null; - } - - /** - * If the user requests help. - * - * @return boolean - */ - public function isHelpRequested() - { - if (empty($this->options) && !$this->defaultConfigExists()) { - return true; - } - - if (isset($this->options['h']) || isset($this->options['help'])) { - return true; - } - - return false; - } - - /** - * Returns help. - * - * @return string - */ - public function getHelp() - { - return <<<"HELP" -Usage: - apigen @option@--config@c <@value@path@c> [options] - apigen @option@--source@c <@value@dir@c|@value@file@c> @option@--destination@c <@value@dir@c> [options] - -Options: - @option@--config@c|@option@-c@c <@value@file@c> Config file - @option@--source@c|@option@-s@c <@value@dir@c|@value@file@c> Source file or directory to parse (can be used multiple times) - @option@--destination@c|@option@-d@c <@value@dir@c> Directory where to save the generated documentation - @option@--extensions@c <@value@list@c> List of allowed file extensions, default "@value@php@c" - @option@--exclude@c <@value@mask@c> Mask (case sensitive) to exclude file or directory from processing (can be used multiple times) - @option@--skip-doc-path@c <@value@mask@c> Don't generate documentation for elements from file or directory with this (case sensitive) mask (can be used multiple times) - @option@--skip-doc-prefix@c <@value@value@c> Don't generate documentation for elements with this (case sensitive) name prefix (can be used multiple times) - @option@--charset@c <@value@list@c> Character set of source files, default "@value@auto@c" - @option@--main@c <@value@value@c> Main project name prefix - @option@--title@c <@value@value@c> Title of generated documentation - @option@--base-url@c <@value@value@c> Documentation base URL - @option@--google-cse-id@c <@value@value@c> Google Custom Search ID - @option@--google-cse-label@c <@value@value@c> Google Custom Search label - @option@--google-analytics@c <@value@value@c> Google Analytics tracking code - @option@--template-config@c <@value@file@c> Template config file, default "@value@{$this->config['templateConfig']}@c" - @option@--allowed-html@c <@value@list@c> List of allowed HTML tags in documentation, default "@value@b,i,a,ul,ol,li,p,br,var,samp,kbd,tt@c" - @option@--groups@c <@value@value@c> How should elements be grouped in the menu. Default value is "@value@auto@c" (namespaces if available, packages otherwise) - @option@--autocomplete@c <@value@list@c> Element types for search input autocomplete. Default value is "@value@classes,constants,functions@c" - @option@--access-levels@c <@value@list@c> Generate documentation for methods and properties with given access level, default "@value@public,protected@c" - @option@--internal@c <@value@yes@c|@value@no@c> Generate documentation for elements marked as internal and display internal documentation parts, default "@value@no@c" - @option@--php@c <@value@yes@c|@value@no@c> Generate documentation for PHP internal classes, default "@value@yes@c" - @option@--tree@c <@value@yes@c|@value@no@c> Generate tree view of classes, interfaces, traits and exceptions, default "@value@yes@c" - @option@--deprecated@c <@value@yes@c|@value@no@c> Generate documentation for deprecated elements, default "@value@no@c" - @option@--todo@c <@value@yes@c|@value@no@c> Generate documentation of tasks, default "@value@no@c" - @option@--source-code@c <@value@yes@c|@value@no@c> Generate highlighted source code files, default "@value@yes@c" - @option@--download@c <@value@yes@c|@value@no@c> Add a link to download documentation as a ZIP archive, default "@value@no@c" - @option@--report@c <@value@file@c> Save a checkstyle report of poorly documented elements into a file - @option@--wipeout@c <@value@yes@c|@value@no@c> Wipe out the destination directory first, default "@value@yes@c" - @option@--quiet@c <@value@yes@c|@value@no@c> Don't display scaning and generating messages, default "@value@no@c" - @option@--progressbar@c <@value@yes@c|@value@no@c> Display progressbars, default "@value@yes@c" - @option@--colors@c <@value@yes@c|@value@no@c> Use colors, default "@value@no@c" on Windows, "@value@yes@c" on other systems - @option@--update-check@c <@value@yes@c|@value@no@c> Check for update, default "@value@yes@c" - @option@--debug@c <@value@yes@c|@value@no@c> Display additional information in case of an error, default "@value@no@c" - @option@--help@c|@option@-h@c Display this help - -Only source and destination directories are required - either set explicitly or using a config file. Configuration parameters passed via command line have precedence over parameters from a config file. - -Boolean options (those with possible values @value@yes@c|@value@no@c) do not have to have their values defined explicitly. Using @option@--debug@c and @option@--debug@c=@value@yes@c is exactly the same. - -Some options can have multiple values. You can do so either by using them multiple times or by separating values by a comma. That means that writing @option@--source@c=@value@file1.php@c @option@--source@c=@value@file2.php@c or @option@--source@c=@value@file1.php,file2.php@c is exactly the same. - -Files or directories specified by @option@--exclude@c will not be processed at all. -Elements from files within @option@--skip-doc-path@c or with @option@--skip-doc-prefix@c will be parsed but will not have their documentation generated. However if classes have any child classes, the full class tree will be generated and their inherited methods, properties and constants will be displayed (but will not be clickable). - -HELP; - } - - /** - * Checks if ApiGen is installed by PEAR. - * - * @return boolean - */ - public static function isInstalledByPear() - { - return false === strpos('@data_dir@', '@data_dir'); - } - - /** - * Checks if ApiGen is installed from downloaded archive. - * - * @return boolean - */ - public static function isInstalledByDownload() - { - return !self::isInstalledByPear(); - } -} diff --git a/apigen/ApiGen/ConfigException.php b/apigen/ApiGen/ConfigException.php deleted file mode 100644 index 07709114b59..00000000000 --- a/apigen/ApiGen/ConfigException.php +++ /dev/null @@ -1,25 +0,0 @@ - '[%s] %\' 6.2f%% %\' 3dMB', - 'width' => 80, - 'bar' => 64, - 'current' => 0, - 'maximum' => 1 - ); - - /** - * Sets configuration. - * - * @param array $config - */ - public function __construct(Config $config) - { - $this->config = $config; - $this->parsedClasses = new \ArrayObject(); - $this->parsedConstants = new \ArrayObject(); - $this->parsedFunctions = new \ArrayObject(); - } - - /** - * Scans and parses PHP files. - * - * @return array - * @throws \RuntimeException If no PHP files have been found. - */ - public function parse() - { - $files = array(); - - $flags = \RecursiveDirectoryIterator::CURRENT_AS_FILEINFO | \RecursiveDirectoryIterator::SKIP_DOTS; - if (defined('\\RecursiveDirectoryIterator::FOLLOW_SYMLINKS')) { - // Available from PHP 5.3.1 - $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - } - - foreach ($this->config->source as $source) { - $entries = array(); - if (is_dir($source)) { - foreach (new \RecursiveIteratorIterator(new SourceFilesFilterIterator(new \RecursiveDirectoryIterator($source, $flags), $this->config->exclude)) as $entry) { - if (!$entry->isFile()) { - continue; - } - $entries[] = $entry; - } - } elseif ($this->isPhar($source)) { - if (!extension_loaded('phar')) { - throw new RuntimeException('Phar extension is not loaded'); - } - foreach (new \RecursiveIteratorIterator(new \Phar($source, $flags)) as $entry) { - if (!$entry->isFile()) { - continue; - } - $entries[] = $entry; - } - } else { - $entries[] = new \SplFileInfo($source); - } - - $regexp = '~\\.' . implode('|', $this->config->extensions) . '$~i'; - foreach ($entries as $entry) { - if (!preg_match($regexp, $entry->getFilename())) { - continue; - } - - $pathName = $this->normalizePath($entry->getPathName()); - $files[$pathName] = $entry->getSize(); - if (false !== $entry->getRealPath() && $pathName !== $entry->getRealPath()) { - $this->symlinks[$entry->getRealPath()] = $pathName; - } - } - } - - if (empty($files)) { - throw new RuntimeException('No PHP files found'); - } - - if ($this->config->progressbar) { - $this->prepareProgressBar(array_sum($files)); - } - - $broker = new Broker(new Backend($this, !empty($this->config->report)), Broker::OPTION_DEFAULT & ~(Broker::OPTION_PARSE_FUNCTION_BODY | Broker::OPTION_SAVE_TOKEN_STREAM)); - - $errors = array(); - - foreach ($files as $fileName => $size) { - $content = file_get_contents($fileName); - $charset = $this->detectCharset($content); - $this->charsets[$fileName] = $charset; - $content = $this->toUtf($content, $charset); - - try { - $broker->processString($content, $fileName); - } catch (\Exception $e) { - $errors[] = $e; - } - - $this->incrementProgressBar($size); - $this->checkMemory(); - } - - // Classes - $this->parsedClasses->exchangeArray($broker->getClasses(Backend::TOKENIZED_CLASSES | Backend::INTERNAL_CLASSES | Backend::NONEXISTENT_CLASSES)); - $this->parsedClasses->uksort('strcasecmp'); - - // Constants - $this->parsedConstants->exchangeArray($broker->getConstants()); - $this->parsedConstants->uksort('strcasecmp'); - - // Functions - $this->parsedFunctions->exchangeArray($broker->getFunctions()); - $this->parsedFunctions->uksort('strcasecmp'); - - $documentedCounter = function($count, $element) { - return $count += (int) $element->isDocumented(); - }; - - return (object) array( - 'classes' => count($broker->getClasses(Backend::TOKENIZED_CLASSES)), - 'constants' => count($this->parsedConstants), - 'functions' => count($this->parsedFunctions), - 'internalClasses' => count($broker->getClasses(Backend::INTERNAL_CLASSES)), - 'documentedClasses' => array_reduce($broker->getClasses(Backend::TOKENIZED_CLASSES), $documentedCounter), - 'documentedConstants' => array_reduce($this->parsedConstants->getArrayCopy(), $documentedCounter), - 'documentedFunctions' => array_reduce($this->parsedFunctions->getArrayCopy(), $documentedCounter), - 'documentedInternalClasses' => array_reduce($broker->getClasses(Backend::INTERNAL_CLASSES), $documentedCounter), - 'errors' => $errors - ); - } - - /** - * Returns configuration. - * - * @return mixed - */ - public function getConfig() - { - return $this->config; - } - - /** - * Returns parsed class list. - * - * @return \ArrayObject - */ - public function getParsedClasses() - { - return $this->parsedClasses; - } - - /** - * Returns parsed constant list. - * - * @return \ArrayObject - */ - public function getParsedConstants() - { - return $this->parsedConstants; - } - - /** - * Returns parsed function list. - * - * @return \ArrayObject - */ - public function getParsedFunctions() - { - return $this->parsedFunctions; - } - - /** - * Wipes out the destination directory. - * - * @return boolean - */ - public function wipeOutDestination() - { - foreach ($this->getGeneratedFiles() as $path) { - if (is_file($path) && !@unlink($path)) { - return false; - } - } - - $archive = $this->getArchivePath(); - if (is_file($archive) && !@unlink($archive)) { - return false; - } - - return true; - } - - /** - * Generates API documentation. - * - * @throws \RuntimeException If destination directory is not writable. - */ - public function generate() - { - @mkdir($this->config->destination, 0755, true); - if (!is_dir($this->config->destination) || !is_writable($this->config->destination)) { - throw new RuntimeException(sprintf('Directory "%s" isn\'t writable', $this->config->destination)); - } - - // Copy resources - foreach ($this->config->template['resources'] as $resourceSource => $resourceDestination) { - // File - $resourcePath = $this->getTemplateDir() . DIRECTORY_SEPARATOR . $resourceSource; - if (is_file($resourcePath)) { - copy($resourcePath, $this->forceDir($this->config->destination . DIRECTORY_SEPARATOR . $resourceDestination)); - continue; - } - - // Dir - $iterator = Nette\Utils\Finder::findFiles('*')->from($resourcePath)->getIterator(); - foreach ($iterator as $item) { - copy($item->getPathName(), $this->forceDir($this->config->destination . DIRECTORY_SEPARATOR . $resourceDestination . DIRECTORY_SEPARATOR . $iterator->getSubPathName())); - } - } - - // Categorize by packages and namespaces - $this->categorize(); - - // Prepare progressbar - if ($this->config->progressbar) { - $max = count($this->packages) - + count($this->namespaces) - + count($this->classes) - + count($this->interfaces) - + count($this->traits) - + count($this->exceptions) - + count($this->constants) - + count($this->functions) - + count($this->config->template['templates']['common']) - + (int) !empty($this->config->report) - + (int) $this->config->tree - + (int) $this->config->deprecated - + (int) $this->config->todo - + (int) $this->config->download - + (int) $this->isSitemapEnabled() - + (int) $this->isOpensearchEnabled() - + (int) $this->isRobotsEnabled(); - - if ($this->config->sourceCode) { - $tokenizedFilter = function(ReflectionClass $class) { - return $class->isTokenized(); - }; - $max += count(array_filter($this->classes, $tokenizedFilter)) - + count(array_filter($this->interfaces, $tokenizedFilter)) - + count(array_filter($this->traits, $tokenizedFilter)) - + count(array_filter($this->exceptions, $tokenizedFilter)) - + count($this->constants) - + count($this->functions); - unset($tokenizedFilter); - } - - $this->prepareProgressBar($max); - } - - // Prepare template - $tmp = $this->config->destination . DIRECTORY_SEPARATOR . 'tmp'; - $this->deleteDir($tmp); - @mkdir($tmp, 0755, true); - $template = new Template($this); - $template->setCacheStorage(new Nette\Caching\Storages\PhpFileStorage($tmp)); - $template->generator = self::NAME; - $template->version = self::VERSION; - $template->config = $this->config; - - $this->registerCustomTemplateMacros($template); - - // Common files - $this->generateCommon($template); - - // Optional files - $this->generateOptional($template); - - // List of poorly documented elements - if (!empty($this->config->report)) { - $this->generateReport(); - } - - // List of deprecated elements - if ($this->config->deprecated) { - $this->generateDeprecated($template); - } - - // List of tasks - if ($this->config->todo) { - $this->generateTodo($template); - } - - // Classes/interfaces/traits/exceptions tree - if ($this->config->tree) { - $this->generateTree($template); - } - - // Generate packages summary - $this->generatePackages($template); - - // Generate namespaces summary - $this->generateNamespaces($template); - - // Generate classes, interfaces, traits, exceptions, constants and functions files - $this->generateElements($template); - - // Generate ZIP archive - if ($this->config->download) { - $this->generateArchive(); - } - - // Delete temporary directory - $this->deleteDir($tmp); - } - - /** - * Loads template-specific macro and helper libraries. - * - * @param \ApiGen\Template $template Template instance - */ - private function registerCustomTemplateMacros(Template $template) - { - $latte = new Nette\Latte\Engine(); - - if (!empty($this->config->template['options']['extensions'])) { - $this->output("Loading custom template macro and helper libraries\n"); - $broker = new Broker(new Broker\Backend\Memory(), 0); - - $baseDir = dirname($this->config->template['config']); - foreach ((array) $this->config->template['options']['extensions'] as $fileName) { - $pathName = $baseDir . DIRECTORY_SEPARATOR . $fileName; - if (is_file($pathName)) { - try { - $reflectionFile = $broker->processFile($pathName, true); - - foreach ($reflectionFile->getNamespaces() as $namespace) { - foreach ($namespace->getClasses() as $class) { - if ($class->isSubclassOf('ApiGen\\MacroSet')) { - // Macro set - - include $pathName; - call_user_func(array($class->getName(), 'install'), $latte->compiler); - - $this->output(sprintf(" %s (macro set)\n", $class->getName())); - } elseif ($class->implementsInterface('ApiGen\\IHelperSet')) { - // Helpers set - - include $pathName; - $className = $class->getName(); - $template->registerHelperLoader(callback(new $className($template), 'loader')); - - $this->output(sprintf(" %s (helper set)\n", $class->getName())); - } - } - } - } catch (\Exception $e) { - throw new \Exception(sprintf('Could not load macros and helpers from file "%s"', $pathName), 0, $e); - } - } else { - throw new \Exception(sprintf('Helper file "%s" does not exist.', $pathName)); - } - } - } - - $template->registerFilter($latte); - } - - /** - * Categorizes by packages and namespaces. - * - * @return \ApiGen\Generator - */ - private function categorize() - { - foreach (array('classes', 'constants', 'functions') as $type) { - foreach ($this->{'parsed' . ucfirst($type)} as $elementName => $element) { - if (!$element->isDocumented()) { - continue; - } - - $packageName = $element->getPseudoPackageName(); - $namespaceName = $element->getPseudoNamespaceName(); - - if ($element instanceof ReflectionConstant) { - $this->constants[$elementName] = $element; - $this->packages[$packageName]['constants'][$elementName] = $element; - $this->namespaces[$namespaceName]['constants'][$element->getShortName()] = $element; - } elseif ($element instanceof ReflectionFunction) { - $this->functions[$elementName] = $element; - $this->packages[$packageName]['functions'][$elementName] = $element; - $this->namespaces[$namespaceName]['functions'][$element->getShortName()] = $element; - } elseif ($element->isInterface()) { - $this->interfaces[$elementName] = $element; - $this->packages[$packageName]['interfaces'][$elementName] = $element; - $this->namespaces[$namespaceName]['interfaces'][$element->getShortName()] = $element; - } elseif ($element->isTrait()) { - $this->traits[$elementName] = $element; - $this->packages[$packageName]['traits'][$elementName] = $element; - $this->namespaces[$namespaceName]['traits'][$element->getShortName()] = $element; - } elseif ($element->isException()) { - $this->exceptions[$elementName] = $element; - $this->packages[$packageName]['exceptions'][$elementName] = $element; - $this->namespaces[$namespaceName]['exceptions'][$element->getShortName()] = $element; - } else { - $this->classes[$elementName] = $element; - $this->packages[$packageName]['classes'][$elementName] = $element; - $this->namespaces[$namespaceName]['classes'][$element->getShortName()] = $element; - } - } - } - - // Select only packages or namespaces - $userPackagesCount = count(array_diff(array_keys($this->packages), array('PHP', 'None'))); - $userNamespacesCount = count(array_diff(array_keys($this->namespaces), array('PHP', 'None'))); - - $namespacesEnabled = ('auto' === $this->config->groups && ($userNamespacesCount > 0 || 0 === $userPackagesCount)) || 'namespaces' === $this->config->groups; - $packagesEnabled = ('auto' === $this->config->groups && !$namespacesEnabled) || 'packages' === $this->config->groups; - - if ($namespacesEnabled) { - $this->packages = array(); - $this->namespaces = $this->sortGroups($this->namespaces); - } elseif ($packagesEnabled) { - $this->namespaces = array(); - $this->packages = $this->sortGroups($this->packages); - } else { - $this->namespaces = array(); - $this->packages = array(); - } - - return $this; - } - - /** - * Sorts and filters groups. - * - * @param array $groups - * @return array - */ - private function sortGroups(array $groups) - { - // Don't generate only 'None' groups - if (1 === count($groups) && isset($groups['None'])) { - return array(); - } - - $emptyList = array('classes' => array(), 'interfaces' => array(), 'traits' => array(), 'exceptions' => array(), 'constants' => array(), 'functions' => array()); - - $groupNames = array_keys($groups); - $lowerGroupNames = array_flip(array_map(function($y) { - return strtolower($y); - }, $groupNames)); - - foreach ($groupNames as $groupName) { - // Add missing parent groups - $parent = ''; - foreach (explode('\\', $groupName) as $part) { - $parent = ltrim($parent . '\\' . $part, '\\'); - if (!isset($lowerGroupNames[strtolower($parent)])) { - $groups[$parent] = $emptyList; - } - } - - // Add missing element types - foreach ($this->getElementTypes() as $type) { - if (!isset($groups[$groupName][$type])) { - $groups[$groupName][$type] = array(); - } - } - } - - $main = $this->config->main; - uksort($groups, function($one, $two) use ($main) { - // \ as separator has to be first - $one = str_replace('\\', ' ', $one); - $two = str_replace('\\', ' ', $two); - - if ($main) { - if (0 === strpos($one, $main) && 0 !== strpos($two, $main)) { - return -1; - } elseif (0 !== strpos($one, $main) && 0 === strpos($two, $main)) { - return 1; - } - } - - return strcasecmp($one, $two); - }); - - return $groups; - } - - /** - * Generates common files. - * - * @param \ApiGen\Template $template Template - * @return \ApiGen\Generator - */ - private function generateCommon(Template $template) - { - $template->namespace = null; - $template->namespaces = array_keys($this->namespaces); - $template->package = null; - $template->packages = array_keys($this->packages); - $template->class = null; - $template->classes = array_filter($this->classes, $this->getMainFilter()); - $template->interfaces = array_filter($this->interfaces, $this->getMainFilter()); - $template->traits = array_filter($this->traits, $this->getMainFilter()); - $template->exceptions = array_filter($this->exceptions, $this->getMainFilter()); - $template->constant = null; - $template->constants = array_filter($this->constants, $this->getMainFilter()); - $template->function = null; - $template->functions = array_filter($this->functions, $this->getMainFilter()); - $template->archive = basename($this->getArchivePath()); - - // Elements for autocomplete - $elements = array(); - $autocomplete = array_flip($this->config->autocomplete); - foreach ($this->getElementTypes() as $type) { - foreach ($this->$type as $element) { - if ($element instanceof ReflectionClass) { - if (isset($autocomplete['classes'])) { - $elements[] = array('c', $element->getPrettyName()); - } - if (isset($autocomplete['methods'])) { - foreach ($element->getOwnMethods() as $method) { - $elements[] = array('m', $method->getPrettyName()); - } - foreach ($element->getOwnMagicMethods() as $method) { - $elements[] = array('mm', $method->getPrettyName()); - } - } - if (isset($autocomplete['properties'])) { - foreach ($element->getOwnProperties() as $property) { - $elements[] = array('p', $property->getPrettyName()); - } - foreach ($element->getOwnMagicProperties() as $property) { - $elements[] = array('mp', $property->getPrettyName()); - } - } - if (isset($autocomplete['classconstants'])) { - foreach ($element->getOwnConstants() as $constant) { - $elements[] = array('cc', $constant->getPrettyName()); - } - } - } elseif ($element instanceof ReflectionConstant && isset($autocomplete['constants'])) { - $elements[] = array('co', $element->getPrettyName()); - } elseif ($element instanceof ReflectionFunction && isset($autocomplete['functions'])) { - $elements[] = array('f', $element->getPrettyName()); - } - } - } - usort($elements, function($one, $two) { - return strcasecmp($one[1], $two[1]); - }); - $template->elements = $elements; - - foreach ($this->config->template['templates']['common'] as $source => $destination) { - $template - ->setFile($this->getTemplateDir() . DIRECTORY_SEPARATOR . $source) - ->save($this->forceDir($this->config->destination . DIRECTORY_SEPARATOR . $destination)); - - $this->incrementProgressBar(); - } - - unset($template->elements); - - $this->checkMemory(); - - return $this; - } - - /** - * Generates optional files. - * - * @param \ApiGen\Template $template Template - * @return \ApiGen\Generator - */ - private function generateOptional(Template $template) - { - if ($this->isSitemapEnabled()) { - $template - ->setFile($this->getTemplatePath('sitemap', 'optional')) - ->save($this->forceDir($this->getTemplateFileName('sitemap', 'optional'))); - $this->incrementProgressBar(); - } - if ($this->isOpensearchEnabled()) { - $template - ->setFile($this->getTemplatePath('opensearch', 'optional')) - ->save($this->forceDir($this->getTemplateFileName('opensearch', 'optional'))); - $this->incrementProgressBar(); - } - if ($this->isRobotsEnabled()) { - $template - ->setFile($this->getTemplatePath('robots', 'optional')) - ->save($this->forceDir($this->getTemplateFileName('robots', 'optional'))); - $this->incrementProgressBar(); - } - - $this->checkMemory(); - - return $this; - } - - /** - * Generates list of poorly documented elements. - * - * @return \ApiGen\Generator - * @throws \RuntimeException If file isn't writable. - */ - private function generateReport() - { - // Function for element labels - $that = $this; - $labeler = function($element) use ($that) { - if ($element instanceof ReflectionClass) { - if ($element->isInterface()) { - $label = 'interface'; - } elseif ($element->isTrait()) { - $label = 'trait'; - } elseif ($element->isException()) { - $label = 'exception'; - } else { - $label = 'class'; - } - } elseif ($element instanceof ReflectionMethod) { - $label = 'method'; - } elseif ($element instanceof ReflectionFunction) { - $label = 'function'; - } elseif ($element instanceof ReflectionConstant) { - $label = 'constant'; - } elseif ($element instanceof ReflectionProperty) { - $label = 'property'; - } elseif ($element instanceof ReflectionParameter) { - $label = 'parameter'; - } - return sprintf('%s %s', $label, $element->getPrettyName()); - }; - - $list = array(); - foreach ($this->getElementTypes() as $type) { - foreach ($this->$type as $parentElement) { - $fileName = $this->unPharPath($parentElement->getFileName()); - - if (!$parentElement->isValid()) { - $list[$fileName][] = array('error', 0, sprintf('Duplicate %s', $labeler($parentElement))); - continue; - } - - // Skip elements not from the main project - if (!$parentElement->isMain()) { - continue; - } - - // Internal elements don't have documentation - if ($parentElement->isInternal()) { - continue; - } - - $elements = array($parentElement); - if ($parentElement instanceof ReflectionClass) { - $elements = array_merge( - $elements, - array_values($parentElement->getOwnMethods()), - array_values($parentElement->getOwnConstants()), - array_values($parentElement->getOwnProperties()) - ); - } - - $tokens = $parentElement->getBroker()->getFileTokens($parentElement->getFileName()); - - foreach ($elements as $element) { - $line = $element->getStartLine(); - $label = $labeler($element); - - $annotations = $element->getAnnotations(); - - // Documentation - if (empty($element->shortDescription)) { - if (empty($annotations)) { - $list[$fileName][] = array('error', $line, sprintf('Missing documentation of %s', $label)); - continue; - } - // Description - $list[$fileName][] = array('error', $line, sprintf('Missing description of %s', $label)); - } - - // Documentation of method - if ($element instanceof ReflectionMethod || $element instanceof ReflectionFunction) { - // Parameters - $unlimited = false; - foreach ($element->getParameters() as $no => $parameter) { - if (!isset($annotations['param'][$no])) { - $list[$fileName][] = array('error', $line, sprintf('Missing documentation of %s', $labeler($parameter))); - continue; - } - - if (!preg_match('~^[\\w\\\\]+(?:\\[\\])?(?:\\|[\\w\\\\]+(?:\\[\\])?)*(?:\\s+\\$' . $parameter->getName() . ($parameter->isUnlimited() ? ',\\.{3}' : '') . ')?(?:\\s+.+)?$~s', $annotations['param'][$no])) { - $list[$fileName][] = array('warning', $line, sprintf('Invalid documentation "%s" of %s', $annotations['param'][$no], $labeler($parameter))); - } - - if ($unlimited && $parameter->isUnlimited()) { - $list[$fileName][] = array('warning', $line, sprintf('More than one unlimited parameters of %s', $labeler($element))); - } elseif ($parameter->isUnlimited()) { - $unlimited = true; - } - - unset($annotations['param'][$no]); - } - if (isset($annotations['param'])) { - foreach ($annotations['param'] as $annotation) { - $list[$fileName][] = array('warning', $line, sprintf('Existing documentation "%s" of nonexistent parameter of %s', $annotation, $label)); - } - } - - // Return values - $return = false; - $tokens->seek($element->getStartPosition()) - ->find(T_FUNCTION); - while ($tokens->next() && $tokens->key() < $element->getEndPosition()) { - $type = $tokens->getType(); - if (T_FUNCTION === $type) { - // Skip annonymous functions - $tokens->find('{')->findMatchingBracket(); - } elseif (T_RETURN === $type && !$tokens->skipWhitespaces()->is(';')) { - // Skip return without return value - $return = true; - break; - } - } - if ($return && !isset($annotations['return'])) { - $list[$fileName][] = array('error', $line, sprintf('Missing documentation of return value of %s', $label)); - } elseif (isset($annotations['return'])) { - if (!$return && 'void' !== $annotations['return'][0] && ($element instanceof ReflectionFunction || (!$parentElement->isInterface() && !$element->isAbstract()))) { - $list[$fileName][] = array('warning', $line, sprintf('Existing documentation "%s" of nonexistent return value of %s', $annotations['return'][0], $label)); - } elseif (!preg_match('~^[\\w\\\\]+(?:\\[\\])?(?:\\|[\\w\\\\]+(?:\\[\\])?)*(?:\\s+.+)?$~s', $annotations['return'][0])) { - $list[$fileName][] = array('warning', $line, sprintf('Invalid documentation "%s" of return value of %s', $annotations['return'][0], $label)); - } - } - if (isset($annotations['return'][1])) { - $list[$fileName][] = array('warning', $line, sprintf('Duplicate documentation "%s" of return value of %s', $annotations['return'][1], $label)); - } - - // Throwing exceptions - $throw = false; - $tokens->seek($element->getStartPosition()) - ->find(T_FUNCTION); - while ($tokens->next() && $tokens->key() < $element->getEndPosition()) { - $type = $tokens->getType(); - if (T_TRY === $type) { - // Skip try - $tokens->find('{')->findMatchingBracket(); - } elseif (T_THROW === $type) { - $throw = true; - break; - } - } - if ($throw && !isset($annotations['throws'])) { - $list[$fileName][] = array('error', $line, sprintf('Missing documentation of throwing an exception of %s', $label)); - } elseif (isset($annotations['throws']) && !preg_match('~^[\\w\\\\]+(?:\\|[\\w\\\\]+)*(?:\\s+.+)?$~s', $annotations['throws'][0])) { - $list[$fileName][] = array('warning', $line, sprintf('Invalid documentation "%s" of throwing an exception of %s', $annotations['throws'][0], $label)); - } - } - - // Data type of constants & properties - if ($element instanceof ReflectionProperty || $element instanceof ReflectionConstant) { - if (!isset($annotations['var'])) { - $list[$fileName][] = array('error', $line, sprintf('Missing documentation of the data type of %s', $label)); - } elseif (!preg_match('~^[\\w\\\\]+(?:\\[\\])?(?:\\|[\\w\\\\]+(?:\\[\\])?)*(?:\\s+.+)?$~s', $annotations['var'][0])) { - $list[$fileName][] = array('warning', $line, sprintf('Invalid documentation "%s" of the data type of %s', $annotations['var'][0], $label)); - } - - if (isset($annotations['var'][1])) { - $list[$fileName][] = array('warning', $line, sprintf('Duplicate documentation "%s" of the data type of %s', $annotations['var'][1], $label)); - } - } - } - unset($tokens); - } - } - uksort($list, 'strcasecmp'); - - $file = @fopen($this->config->report, 'w'); - if (false === $file) { - throw new RuntimeException(sprintf('File "%s" isn\'t writable', $this->config->report)); - } - fwrite($file, sprintf('%s', "\n")); - fwrite($file, sprintf('%s', "\n")); - foreach ($list as $fileName => $reports) { - fwrite($file, sprintf('%s%s', "\t", $fileName, "\n")); - - // Sort by line - usort($reports, function($one, $two) { - return strnatcmp($one[1], $two[1]); - }); - - foreach ($reports as $report) { - list($severity, $line, $message) = $report; - $message = preg_replace('~\\s+~u', ' ', $message); - fwrite($file, sprintf('%s%s', "\t\t", $severity, $line, htmlspecialchars($message), "\n")); - } - - fwrite($file, sprintf('%s%s', "\t", "\n")); - } - fwrite($file, sprintf('%s', "\n")); - fclose($file); - - $this->incrementProgressBar(); - $this->checkMemory(); - - return $this; - } - - /** - * Generates list of deprecated elements. - * - * @param \ApiGen\Template $template Template - * @return \ApiGen\Generator - * @throws \RuntimeException If template is not set. - */ - private function generateDeprecated(Template $template) - { - $this->prepareTemplate('deprecated'); - - $deprecatedFilter = function($element) { - return $element->isDeprecated(); - }; - - $template->deprecatedMethods = array(); - $template->deprecatedConstants = array(); - $template->deprecatedProperties = array(); - foreach (array_reverse($this->getElementTypes()) as $type) { - $template->{'deprecated' . ucfirst($type)} = array_filter(array_filter($this->$type, $this->getMainFilter()), $deprecatedFilter); - - if ('constants' === $type || 'functions' === $type) { - continue; - } - - foreach ($this->$type as $class) { - if (!$class->isMain()) { - continue; - } - - if ($class->isDeprecated()) { - continue; - } - - $template->deprecatedMethods = array_merge($template->deprecatedMethods, array_values(array_filter($class->getOwnMethods(), $deprecatedFilter))); - $template->deprecatedConstants = array_merge($template->deprecatedConstants, array_values(array_filter($class->getOwnConstants(), $deprecatedFilter))); - $template->deprecatedProperties = array_merge($template->deprecatedProperties, array_values(array_filter($class->getOwnProperties(), $deprecatedFilter))); - } - } - usort($template->deprecatedMethods, array($this, 'sortMethods')); - usort($template->deprecatedConstants, array($this, 'sortConstants')); - usort($template->deprecatedFunctions, array($this, 'sortFunctions')); - usort($template->deprecatedProperties, array($this, 'sortProperties')); - - $template - ->setFile($this->getTemplatePath('deprecated')) - ->save($this->forceDir($this->getTemplateFileName('deprecated'))); - - foreach ($this->getElementTypes() as $type) { - unset($template->{'deprecated' . ucfirst($type)}); - } - unset($template->deprecatedMethods); - unset($template->deprecatedProperties); - - $this->incrementProgressBar(); - $this->checkMemory(); - - return $this; - } - - /** - * Generates list of tasks. - * - * @param \ApiGen\Template $template Template - * @return \ApiGen\Generator - * @throws \RuntimeException If template is not set. - */ - private function generateTodo(Template $template) - { - $this->prepareTemplate('todo'); - - $todoFilter = function($element) { - return $element->hasAnnotation('todo'); - }; - - $template->todoMethods = array(); - $template->todoConstants = array(); - $template->todoProperties = array(); - foreach (array_reverse($this->getElementTypes()) as $type) { - $template->{'todo' . ucfirst($type)} = array_filter(array_filter($this->$type, $this->getMainFilter()), $todoFilter); - - if ('constants' === $type || 'functions' === $type) { - continue; - } - - foreach ($this->$type as $class) { - if (!$class->isMain()) { - continue; - } - - $template->todoMethods = array_merge($template->todoMethods, array_values(array_filter($class->getOwnMethods(), $todoFilter))); - $template->todoConstants = array_merge($template->todoConstants, array_values(array_filter($class->getOwnConstants(), $todoFilter))); - $template->todoProperties = array_merge($template->todoProperties, array_values(array_filter($class->getOwnProperties(), $todoFilter))); - } - } - usort($template->todoMethods, array($this, 'sortMethods')); - usort($template->todoConstants, array($this, 'sortConstants')); - usort($template->todoFunctions, array($this, 'sortFunctions')); - usort($template->todoProperties, array($this, 'sortProperties')); - - $template - ->setFile($this->getTemplatePath('todo')) - ->save($this->forceDir($this->getTemplateFileName('todo'))); - - foreach ($this->getElementTypes() as $type) { - unset($template->{'todo' . ucfirst($type)}); - } - unset($template->todoMethods); - unset($template->todoProperties); - - $this->incrementProgressBar(); - $this->checkMemory(); - - return $this; - } - - /** - * Generates classes/interfaces/traits/exceptions tree. - * - * @param \ApiGen\Template $template Template - * @return \ApiGen\Generator - * @throws \RuntimeException If template is not set. - */ - private function generateTree(Template $template) - { - $this->prepareTemplate('tree'); - - $classTree = array(); - $interfaceTree = array(); - $traitTree = array(); - $exceptionTree = array(); - - $processed = array(); - foreach ($this->parsedClasses as $className => $reflection) { - if (!$reflection->isMain() || !$reflection->isDocumented() || isset($processed[$className])) { - continue; - } - - if (null === $reflection->getParentClassName()) { - // No parent classes - if ($reflection->isInterface()) { - $t = &$interfaceTree; - } elseif ($reflection->isTrait()) { - $t = &$traitTree; - } elseif ($reflection->isException()) { - $t = &$exceptionTree; - } else { - $t = &$classTree; - } - } else { - foreach (array_values(array_reverse($reflection->getParentClasses())) as $level => $parent) { - if (0 === $level) { - // The topmost parent decides about the reflection type - if ($parent->isInterface()) { - $t = &$interfaceTree; - } elseif ($parent->isTrait()) { - $t = &$traitTree; - } elseif ($parent->isException()) { - $t = &$exceptionTree; - } else { - $t = &$classTree; - } - } - $parentName = $parent->getName(); - - if (!isset($t[$parentName])) { - $t[$parentName] = array(); - $processed[$parentName] = true; - ksort($t, SORT_STRING); - } - - $t = &$t[$parentName]; - } - } - $t[$className] = array(); - ksort($t, SORT_STRING); - $processed[$className] = true; - unset($t); - } - - $template->classTree = new Tree($classTree, $this->parsedClasses); - $template->interfaceTree = new Tree($interfaceTree, $this->parsedClasses); - $template->traitTree = new Tree($traitTree, $this->parsedClasses); - $template->exceptionTree = new Tree($exceptionTree, $this->parsedClasses); - - $template - ->setFile($this->getTemplatePath('tree')) - ->save($this->forceDir($this->getTemplateFileName('tree'))); - - unset($template->classTree); - unset($template->interfaceTree); - unset($template->traitTree); - unset($template->exceptionTree); - - $this->incrementProgressBar(); - $this->checkMemory(); - - return $this; - } - - /** - * Generates packages summary. - * - * @param \ApiGen\Template $template Template - * @return \ApiGen\Generator - * @throws \RuntimeException If template is not set. - */ - private function generatePackages(Template $template) - { - if (empty($this->packages)) { - return $this; - } - - $this->prepareTemplate('package'); - - $template->namespace = null; - - foreach ($this->packages as $packageName => $package) { - $template->package = $packageName; - $template->subpackages = array_filter($template->packages, function($subpackageName) use ($packageName) { - return (bool) preg_match('~^' . preg_quote($packageName) . '\\\\[^\\\\]+$~', $subpackageName); - }); - $template->classes = $package['classes']; - $template->interfaces = $package['interfaces']; - $template->traits = $package['traits']; - $template->exceptions = $package['exceptions']; - $template->constants = $package['constants']; - $template->functions = $package['functions']; - $template - ->setFile($this->getTemplatePath('package')) - ->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getPackageUrl($packageName)); - - $this->incrementProgressBar(); - } - unset($template->subpackages); - - $this->checkMemory(); - - return $this; - } - - /** - * Generates namespaces summary. - * - * @param \ApiGen\Template $template Template - * @return \ApiGen\Generator - * @throws \RuntimeException If template is not set. - */ - private function generateNamespaces(Template $template) - { - if (empty($this->namespaces)) { - return $this; - } - - $this->prepareTemplate('namespace'); - - $template->package = null; - - foreach ($this->namespaces as $namespaceName => $namespace) { - $template->namespace = $namespaceName; - $template->subnamespaces = array_filter($template->namespaces, function($subnamespaceName) use ($namespaceName) { - return (bool) preg_match('~^' . preg_quote($namespaceName) . '\\\\[^\\\\]+$~', $subnamespaceName); - }); - $template->classes = $namespace['classes']; - $template->interfaces = $namespace['interfaces']; - $template->traits = $namespace['traits']; - $template->exceptions = $namespace['exceptions']; - $template->constants = $namespace['constants']; - $template->functions = $namespace['functions']; - $template - ->setFile($this->getTemplatePath('namespace')) - ->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getNamespaceUrl($namespaceName)); - - $this->incrementProgressBar(); - } - unset($template->subnamespaces); - - $this->checkMemory(); - - return $this; - } - - /** - * Generate classes, interfaces, traits, exceptions, constants and functions files. - * - * @param Template $template Template - * @return \ApiGen\Generator - * @throws \RuntimeException If template is not set. - */ - private function generateElements(Template $template) - { - if (!empty($this->classes) || !empty($this->interfaces) || !empty($this->traits) || !empty($this->exceptions)) { - $this->prepareTemplate('class'); - } - if (!empty($this->constants)) { - $this->prepareTemplate('constant'); - } - if (!empty($this->functions)) { - $this->prepareTemplate('function'); - } - if ($this->config->sourceCode) { - $this->prepareTemplate('source'); - - $fshl = new FSHL\Highlighter(new FSHL\Output\Html(), FSHL\Highlighter::OPTION_TAB_INDENT | FSHL\Highlighter::OPTION_LINE_COUNTER); - $fshl->setLexer(new FSHL\Lexer\Php()); - } - - // Add @usedby annotation - foreach ($this->getElementTypes() as $type) { - foreach ($this->$type as $parentElement) { - $elements = array($parentElement); - if ($parentElement instanceof ReflectionClass) { - $elements = array_merge( - $elements, - array_values($parentElement->getOwnMethods()), - array_values($parentElement->getOwnConstants()), - array_values($parentElement->getOwnProperties()) - ); - } - foreach ($elements as $element) { - $uses = $element->getAnnotation('uses'); - if (null === $uses) { - continue; - } - foreach ($uses as $value) { - list($link, $description) = preg_split('~\s+|$~', $value, 2); - $resolved = $this->resolveElement($link, $element); - if (null !== $resolved) { - $resolved->addAnnotation('usedby', $element->getPrettyName() . ' ' . $description); - } - } - } - } - } - - $template->package = null; - $template->namespace = null; - $template->classes = $this->classes; - $template->interfaces = $this->interfaces; - $template->traits = $this->traits; - $template->exceptions = $this->exceptions; - $template->constants = $this->constants; - $template->functions = $this->functions; - foreach ($this->getElementTypes() as $type) { - foreach ($this->$type as $element) { - if (!empty($this->namespaces)) { - $template->namespace = $namespaceName = $element->getPseudoNamespaceName(); - $template->classes = $this->namespaces[$namespaceName]['classes']; - $template->interfaces = $this->namespaces[$namespaceName]['interfaces']; - $template->traits = $this->namespaces[$namespaceName]['traits']; - $template->exceptions = $this->namespaces[$namespaceName]['exceptions']; - $template->constants = $this->namespaces[$namespaceName]['constants']; - $template->functions = $this->namespaces[$namespaceName]['functions']; - } elseif (!empty($this->packages)) { - $template->package = $packageName = $element->getPseudoPackageName(); - $template->classes = $this->packages[$packageName]['classes']; - $template->interfaces = $this->packages[$packageName]['interfaces']; - $template->traits = $this->packages[$packageName]['traits']; - $template->exceptions = $this->packages[$packageName]['exceptions']; - $template->constants = $this->packages[$packageName]['constants']; - $template->functions = $this->packages[$packageName]['functions']; - } - - $template->class = null; - $template->constant = null; - $template->function = null; - if ($element instanceof ReflectionClass) { - // Class - $template->tree = array_merge(array_reverse($element->getParentClasses()), array($element)); - - $template->directSubClasses = $element->getDirectSubClasses(); - uksort($template->directSubClasses, 'strcasecmp'); - $template->indirectSubClasses = $element->getIndirectSubClasses(); - uksort($template->indirectSubClasses, 'strcasecmp'); - - $template->directImplementers = $element->getDirectImplementers(); - uksort($template->directImplementers, 'strcasecmp'); - $template->indirectImplementers = $element->getIndirectImplementers(); - uksort($template->indirectImplementers, 'strcasecmp'); - - $template->directUsers = $element->getDirectUsers(); - uksort($template->directUsers, 'strcasecmp'); - $template->indirectUsers = $element->getIndirectUsers(); - uksort($template->indirectUsers, 'strcasecmp'); - - $template->class = $element; - - $template - ->setFile($this->getTemplatePath('class')) - ->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getClassUrl($element)); - } elseif ($element instanceof ReflectionConstant) { - // Constant - $template->constant = $element; - - $template - ->setFile($this->getTemplatePath('constant')) - ->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getConstantUrl($element)); - } elseif ($element instanceof ReflectionFunction) { - // Function - $template->function = $element; - - $template - ->setFile($this->getTemplatePath('function')) - ->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getFunctionUrl($element)); - } - - $this->incrementProgressBar(); - - // Generate source codes - if ($this->config->sourceCode && $element->isTokenized()) { - $template->fileName = $this->getRelativePath($element->getFileName()); - $template->source = $fshl->highlight($this->toUtf(file_get_contents($element->getFileName()), $this->charsets[$element->getFileName()])); - $template - ->setFile($this->getTemplatePath('source')) - ->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getSourceUrl($element, false)); - - $this->incrementProgressBar(); - } - - $this->checkMemory(); - } - } - - return $this; - } - - /** - * Creates ZIP archive. - * - * @return \ApiGen\Generator - * @throws \RuntimeException If something went wrong. - */ - private function generateArchive() - { - if (!extension_loaded('zip')) { - throw new RuntimeException('Extension zip is not loaded'); - } - - $archive = new \ZipArchive(); - if (true !== $archive->open($this->getArchivePath(), \ZipArchive::CREATE)) { - throw new RuntimeException('Could not open ZIP archive'); - } - - $archive->setArchiveComment(trim(sprintf('%s API documentation generated by %s %s on %s', $this->config->title, self::NAME, self::VERSION, date('Y-m-d H:i:s')))); - - $directory = Nette\Utils\Strings::webalize(trim(sprintf('%s API documentation', $this->config->title)), null, false); - $destinationLength = strlen($this->config->destination); - foreach ($this->getGeneratedFiles() as $file) { - if (is_file($file)) { - $archive->addFile($file, $directory . DIRECTORY_SEPARATOR . substr($file, $destinationLength + 1)); - } - } - - if (false === $archive->close()) { - throw new RuntimeException('Could not save ZIP archive'); - } - - $this->incrementProgressBar(); - $this->checkMemory(); - - return $this; - } - - /** - * Tries to resolve string as class, interface or exception name. - * - * @param string $className Class name description - * @param string $namespace Namespace name - * @return \ApiGen\ReflectionClass - */ - public function getClass($className, $namespace = '') - { - if (isset($this->parsedClasses[$namespace . '\\' . $className])) { - $class = $this->parsedClasses[$namespace . '\\' . $className]; - } elseif (isset($this->parsedClasses[ltrim($className, '\\')])) { - $class = $this->parsedClasses[ltrim($className, '\\')]; - } else { - return null; - } - - // Class is not "documented" - if (!$class->isDocumented()) { - return null; - } - - return $class; - } - - /** - * Tries to resolve type as constant name. - * - * @param string $constantName Constant name - * @param string $namespace Namespace name - * @return \ApiGen\ReflectionConstant - */ - public function getConstant($constantName, $namespace = '') - { - if (isset($this->parsedConstants[$namespace . '\\' . $constantName])) { - $constant = $this->parsedConstants[$namespace . '\\' . $constantName]; - } elseif (isset($this->parsedConstants[ltrim($constantName, '\\')])) { - $constant = $this->parsedConstants[ltrim($constantName, '\\')]; - } else { - return null; - } - - // Constant is not "documented" - if (!$constant->isDocumented()) { - return null; - } - - return $constant; - } - - /** - * Tries to resolve type as function name. - * - * @param string $functionName Function name - * @param string $namespace Namespace name - * @return \ApiGen\ReflectionFunction - */ - public function getFunction($functionName, $namespace = '') - { - if (isset($this->parsedFunctions[$namespace . '\\' . $functionName])) { - $function = $this->parsedFunctions[$namespace . '\\' . $functionName]; - } elseif (isset($this->parsedFunctions[ltrim($functionName, '\\')])) { - $function = $this->parsedFunctions[ltrim($functionName, '\\')]; - } else { - return null; - } - - // Function is not "documented" - if (!$function->isDocumented()) { - return null; - } - - return $function; - } - - /** - * Tries to parse a definition of a class/method/property/constant/function and returns the appropriate instance if successful. - * - * @param string $definition Definition - * @param \ApiGen\ReflectionElement|\ApiGen\ReflectionParameter $context Link context - * @param string $expectedName Expected element name - * @return \ApiGen\ReflectionElement|null - */ - public function resolveElement($definition, $context, &$expectedName = null) - { - // No simple type resolving - static $types = array( - 'boolean' => 1, 'integer' => 1, 'float' => 1, 'string' => 1, - 'array' => 1, 'object' => 1, 'resource' => 1, 'callback' => 1, - 'callable' => 1, 'null' => 1, 'false' => 1, 'true' => 1, 'mixed' => 1 - ); - - if (empty($definition) || isset($types[$definition])) { - return null; - } - - $originalContext = $context; - - if ($context instanceof ReflectionParameter && null === $context->getDeclaringClassName()) { - // Parameter of function in namespace or global space - $context = $this->getFunction($context->getDeclaringFunctionName()); - } elseif ($context instanceof ReflectionMethod || $context instanceof ReflectionParameter - || ($context instanceof ReflectionConstant && null !== $context->getDeclaringClassName()) - || $context instanceof ReflectionProperty - ) { - // Member of a class - $context = $this->getClass($context->getDeclaringClassName()); - } - - if (null === $context) { - return null; - } - - // self, $this references - if ('self' === $definition || '$this' === $definition) { - return $context instanceof ReflectionClass ? $context : null; - } - - $definitionBase = substr($definition, 0, strcspn($definition, '\\:')); - $namespaceAliases = $context->getNamespaceAliases(); - if (!empty($definitionBase) && isset($namespaceAliases[$definitionBase]) && $definition !== ($className = \TokenReflection\Resolver::resolveClassFQN($definition, $namespaceAliases, $context->getNamespaceName()))) { - // Aliased class - $expectedName = $className; - - if (false === strpos($className, ':')) { - return $this->getClass($className, $context->getNamespaceName()); - } else { - $definition = $className; - } - } elseif ($class = $this->getClass($definition, $context->getNamespaceName())) { - // Class - return $class; - } elseif ($constant = $this->getConstant($definition, $context->getNamespaceName())) { - // Constant - return $constant; - } elseif (($function = $this->getFunction($definition, $context->getNamespaceName())) - || ('()' === substr($definition, -2) && ($function = $this->getFunction(substr($definition, 0, -2), $context->getNamespaceName()))) - ) { - // Function - return $function; - } - - if (($pos = strpos($definition, '::')) || ($pos = strpos($definition, '->'))) { - // Class::something or Class->something - if (0 === strpos($definition, 'parent::') && ($parentClassName = $context->getParentClassName())) { - $context = $this->getClass($parentClassName); - } elseif (0 !== strpos($definition, 'self::')) { - $class = $this->getClass(substr($definition, 0, $pos), $context->getNamespaceName()); - - if (null === $class) { - $class = $this->getClass(\TokenReflection\Resolver::resolveClassFQN(substr($definition, 0, $pos), $context->getNamespaceAliases(), $context->getNamespaceName())); - } - - $context = $class; - } - - $definition = substr($definition, $pos + 2); - } elseif ($originalContext instanceof ReflectionParameter) { - return null; - } - - // No usable context - if (null === $context || $context instanceof ReflectionConstant || $context instanceof ReflectionFunction) { - return null; - } - - if ($context->hasProperty($definition)) { - // Class property - return $context->getProperty($definition); - } elseif ('$' === $definition{0} && $context->hasProperty(substr($definition, 1))) { - // Class $property - return $context->getProperty(substr($definition, 1)); - } elseif ($context->hasMethod($definition)) { - // Class method - return $context->getMethod($definition); - } elseif ('()' === substr($definition, -2) && $context->hasMethod(substr($definition, 0, -2))) { - // Class method() - return $context->getMethod(substr($definition, 0, -2)); - } elseif ($context->hasConstant($definition)) { - // Class constant - return $context->getConstant($definition); - } - - return null; - } - - /** - * Prints message if printing is enabled. - * - * @param string $message Output message - */ - public function output($message) - { - if (!$this->config->quiet) { - echo $this->colorize($message); - } - } - - /** - * Colorizes message or removes placeholders if OS doesn't support colors. - * - * @param string $message - * @return string - */ - public function colorize($message) - { - static $placeholders = array( - '@header@' => "\x1b[1;34m", - '@count@' => "\x1b[1;34m", - '@option@' => "\x1b[0;36m", - '@value@' => "\x1b[0;32m", - '@error@' => "\x1b[0;31m", - '@c' => "\x1b[0m" - ); - - if (!$this->config->colors) { - $placeholders = array_fill_keys(array_keys($placeholders), ''); - } - - return strtr($message, $placeholders); - } - - /** - * Returns header. - * - * @return string - */ - public function getHeader() - { - $name = sprintf('%s %s', self::NAME, self::VERSION); - return sprintf("@header@%s@c\n%s\n", $name, str_repeat('-', strlen($name))); - } - - /** - * Removes phar:// from the path. - * - * @param string $path Path - * @return string - */ - public function unPharPath($path) - { - if (0 === strpos($path, 'phar://')) { - $path = substr($path, 7); - } - return $path; - } - - /** - * Adds phar:// to the path. - * - * @param string $path Path - * @return string - */ - private function pharPath($path) - { - return 'phar://' . $path; - } - - /** - * Checks if given path is a phar. - * - * @param string $path - * @return boolean - */ - private function isPhar($path) - { - return (bool) preg_match('~\\.phar(?:\\.zip|\\.tar|(?:(?:\\.tar)?(?:\\.gz|\\.bz2))|$)~i', $path); - } - - /** - * Normalizes directory separators in given path. - * - * @param string $path Path - * @return string - */ - private function normalizePath($path) - { - $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); - $path = str_replace('phar:\\\\', 'phar://', $path); - return $path; - } - - /** - * Prepares the progressbar. - * - * @param integer $maximum Maximum progressbar value - */ - private function prepareProgressBar($maximum = 1) - { - if (!$this->config->progressbar) { - return; - } - - $this->progressbar['current'] = 0; - $this->progressbar['maximum'] = $maximum; - } - - /** - * Increments the progressbar by one. - * - * @param integer $increment Progressbar increment - */ - private function incrementProgressBar($increment = 1) - { - if (!$this->config->progressbar) { - return; - } - - echo str_repeat(chr(0x08), $this->progressbar['width']); - - $this->progressbar['current'] += $increment; - - $percent = $this->progressbar['current'] / $this->progressbar['maximum']; - - $progress = str_pad(str_pad('>', round($percent * $this->progressbar['bar']), '=', STR_PAD_LEFT), $this->progressbar['bar'], ' ', STR_PAD_RIGHT); - - echo sprintf($this->progressbar['skeleton'], $progress, $percent * 100, round(memory_get_usage(true) / 1024 / 1024)); - - if ($this->progressbar['current'] === $this->progressbar['maximum']) { - echo "\n"; - } - } - - /** - * Checks memory usage. - * - * @return \ApiGen\Generator - * @throws \RuntimeException If there is unsufficient reserve of memory. - */ - public function checkMemory() - { - static $limit = null; - if (null === $limit) { - $value = ini_get('memory_limit'); - $unit = substr($value, -1); - if ('-1' === $value) { - $limit = 0; - } elseif ('G' === $unit) { - $limit = (int) $value * 1024 * 1024 * 1024; - } elseif ('M' === $unit) { - $limit = (int) $value * 1024 * 1024; - } else { - $limit = (int) $value; - } - } - - if ($limit && memory_get_usage(true) / $limit >= 0.9) { - throw new RuntimeException(sprintf('Used %d%% of the current memory limit, please increase the limit to generate the whole documentation.', round(memory_get_usage(true) / $limit * 100))); - } - - return $this; - } - - /** - * Detects character set for the given text. - * - * @param string $text Text - * @return string - */ - private function detectCharset($text) - { - // One character set - if (1 === count($this->config->charset) && 'AUTO' !== $this->config->charset[0]) { - return $this->config->charset[0]; - } - - static $charsets = array(); - if (empty($charsets)) { - if (1 === count($this->config->charset) && 'AUTO' === $this->config->charset[0]) { - // Autodetection - $charsets = array( - 'Windows-1251', 'Windows-1252', 'ISO-8859-2', 'ISO-8859-1', 'ISO-8859-3', 'ISO-8859-4', 'ISO-8859-5', 'ISO-8859-6', - 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15' - ); - } else { - // More character sets - $charsets = $this->config->charset; - if (false !== ($key = array_search('WINDOWS-1250', $charsets))) { - // WINDOWS-1250 is not supported - $charsets[$key] = 'ISO-8859-2'; - } - } - // Only supported character sets - $charsets = array_intersect($charsets, mb_list_encodings()); - - // UTF-8 have to be first - array_unshift($charsets, 'UTF-8'); - } - - $charset = mb_detect_encoding($text, $charsets); - // The previous function can not handle WINDOWS-1250 and returns ISO-8859-2 instead - if ('ISO-8859-2' === $charset && preg_match('~[\x7F-\x9F\xBC]~', $text)) { - $charset = 'WINDOWS-1250'; - } - - return $charset; - } - - /** - * Converts text from given character set to UTF-8. - * - * @param string $text Text - * @param string $charset Character set - * @return string - */ - private function toUtf($text, $charset) - { - if ('UTF-8' === $charset) { - return $text; - } - - return @iconv($charset, 'UTF-8//TRANSLIT//IGNORE', $text); - } - - /** - * Checks if sitemap.xml is enabled. - * - * @return boolean - */ - private function isSitemapEnabled() - { - return !empty($this->config->baseUrl) && $this->templateExists('sitemap', 'optional'); - } - - /** - * Checks if opensearch.xml is enabled. - * - * @return boolean - */ - private function isOpensearchEnabled() - { - return !empty($this->config->googleCseId) && !empty($this->config->baseUrl) && $this->templateExists('opensearch', 'optional'); - } - - /** - * Checks if robots.txt is enabled. - * - * @return boolean - */ - private function isRobotsEnabled() - { - return !empty($this->config->baseUrl) && $this->templateExists('robots', 'optional'); - } - - /** - * Sorts methods by FQN. - * - * @param \ApiGen\ReflectionMethod $one - * @param \ApiGen\ReflectionMethod $two - * @return integer - */ - private function sortMethods(ReflectionMethod $one, ReflectionMethod $two) - { - return strcasecmp($one->getDeclaringClassName() . '::' . $one->getName(), $two->getDeclaringClassName() . '::' . $two->getName()); - } - - /** - * Sorts constants by FQN. - * - * @param \ApiGen\ReflectionConstant $one - * @param \ApiGen\ReflectionConstant $two - * @return integer - */ - private function sortConstants(ReflectionConstant $one, ReflectionConstant $two) - { - return strcasecmp(($one->getDeclaringClassName() ?: $one->getNamespaceName()) . '\\' . $one->getName(), ($two->getDeclaringClassName() ?: $two->getNamespaceName()) . '\\' . $two->getName()); - } - - /** - * Sorts functions by FQN. - * - * @param \ApiGen\ReflectionFunction $one - * @param \ApiGen\ReflectionFunction $two - * @return integer - */ - private function sortFunctions(ReflectionFunction $one, ReflectionFunction $two) - { - return strcasecmp($one->getNamespaceName() . '\\' . $one->getName(), $two->getNamespaceName() . '\\' . $two->getName()); - } - - /** - * Sorts functions by FQN. - * - * @param \ApiGen\ReflectionProperty $one - * @param \ApiGen\ReflectionProperty $two - * @return integer - */ - private function sortProperties(ReflectionProperty $one, ReflectionProperty $two) - { - return strcasecmp($one->getDeclaringClassName() . '::' . $one->getName(), $two->getDeclaringClassName() . '::' . $two->getName()); - } - - /** - * Returns list of element types. - * - * @return array - */ - private function getElementTypes() - { - static $types = array('classes', 'interfaces', 'traits', 'exceptions', 'constants', 'functions'); - return $types; - } - - /** - * Returns main filter. - * - * @return \Closure - */ - private function getMainFilter() - { - return function($element) { - return $element->isMain(); - }; - } - - /** - * Returns ZIP archive path. - * - * @return string - */ - private function getArchivePath() - { - $name = trim(sprintf('%s API documentation', $this->config->title)); - return $this->config->destination . DIRECTORY_SEPARATOR . Nette\Utils\Strings::webalize($name) . '.zip'; - } - - /** - * Returns filename relative path to the source directory. - * - * @param string $fileName - * @return string - * @throws \InvalidArgumentException If relative path could not be determined. - */ - public function getRelativePath($fileName) - { - if (isset($this->symlinks[$fileName])) { - $fileName = $this->symlinks[$fileName]; - } - foreach ($this->config->source as $source) { - if ($this->isPhar($source)) { - $source = $this->pharPath($source); - } - if (0 === strpos($fileName, $source)) { - return is_dir($source) ? str_replace('\\', '/', substr($fileName, strlen($source) + 1)) : basename($fileName); - } - } - - throw new InvalidArgumentException(sprintf('Could not determine "%s" relative path', $fileName)); - } - - /** - * Returns template directory. - * - * @return string - */ - private function getTemplateDir() - { - return dirname($this->config->templateConfig); - } - - /** - * Returns template path. - * - * @param string $name Template name - * @param string $type Template type - * @return string - */ - private function getTemplatePath($name, $type = 'main') - { - return $this->getTemplateDir() . DIRECTORY_SEPARATOR . $this->config->template['templates'][$type][$name]['template']; - } - - /** - * Returns template filename. - * - * @param string $name Template name - * @param string $type Template type - * @return string - */ - private function getTemplateFileName($name, $type = 'main') - { - return $this->config->destination . DIRECTORY_SEPARATOR . $this->config->template['templates'][$type][$name]['filename']; - } - - /** - * Checks if template exists. - * - * @param string $name Template name - * @param string $type Template type - * @return string - */ - private function templateExists($name, $type = 'main') - { - return isset($this->config->template['templates'][$type][$name]); - } - - /** - * Checks if template exists and creates dir. - * - * @param string $name - * @throws \RuntimeException If template is not set. - */ - private function prepareTemplate($name) - { - if (!$this->templateExists($name)) { - throw new RuntimeException(sprintf('Template for "%s" is not set', $name)); - } - - $this->forceDir($this->getTemplateFileName($name)); - return $this; - } - - /** - * Returns list of all generated files. - * - * @return array - */ - private function getGeneratedFiles() - { - $files = array(); - - // Resources - foreach ($this->config->template['resources'] as $item) { - $path = $this->getTemplateDir() . DIRECTORY_SEPARATOR . $item; - if (is_dir($path)) { - $iterator = Nette\Utils\Finder::findFiles('*')->from($path)->getIterator(); - foreach ($iterator as $innerItem) { - $files[] = $this->config->destination . DIRECTORY_SEPARATOR . $item . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); - } - } else { - $files[] = $this->config->destination . DIRECTORY_SEPARATOR . $item; - } - } - - // Common files - foreach ($this->config->template['templates']['common'] as $item) { - $files[] = $this->config->destination . DIRECTORY_SEPARATOR . $item; - } - - // Optional files - foreach ($this->config->template['templates']['optional'] as $optional) { - $files[] = $this->config->destination . DIRECTORY_SEPARATOR . $optional['filename']; - } - - // Main files - $masks = array_map(function($config) { - return preg_replace('~%[^%]*?s~', '*', $config['filename']); - }, $this->config->template['templates']['main']); - $filter = function($item) use ($masks) { - foreach ($masks as $mask) { - if (fnmatch($mask, $item->getFilename())) { - return true; - } - } - return false; - }; - - foreach (Nette\Utils\Finder::findFiles('*')->filter($filter)->from($this->config->destination) as $item) { - $files[] = $item->getPathName(); - } - - return $files; - } - - /** - * Ensures a directory is created. - * - * @param string $path Directory path - * @return string - */ - private function forceDir($path) - { - @mkdir(dirname($path), 0755, true); - return $path; - } - - /** - * Deletes a directory. - * - * @param string $path Directory path - * @return boolean - */ - private function deleteDir($path) - { - if (!is_dir($path)) { - return true; - } - - foreach (Nette\Utils\Finder::find('*')->from($path)->childFirst() as $item) { - if ($item->isDir()) { - if (!@rmdir($item)) { - return false; - } - } elseif ($item->isFile()) { - if (!@unlink($item)) { - return false; - } - } - } - if (!@rmdir($path)) { - return false; - } - - return true; - } -} diff --git a/apigen/ApiGen/IHelperSet.php b/apigen/ApiGen/IHelperSet.php deleted file mode 100644 index 911c83820b1..00000000000 --- a/apigen/ApiGen/IHelperSet.php +++ /dev/null @@ -1,28 +0,0 @@ -getConfig(); - self::$parsedClasses = $generator->getParsedClasses(); - self::$parsedConstants = $generator->getParsedConstants(); - self::$parsedFunctions = $generator->getParsedFunctions(); - } - - $this->reflectionType = get_class($this); - if (!isset(self::$reflectionMethods[$this->reflectionType])) { - self::$reflectionMethods[$this->reflectionType] = array_flip(get_class_methods($this)); - } - - $this->reflection = $reflection; - } - - /** - * Retrieves a property or method value. - * - * First tries the envelope object's property storage, then its methods - * and finally the inspected element reflection. - * - * @param string $name Property name - * @return mixed - */ - public function __get($name) - { - $key = ucfirst($name); - if (isset(self::$reflectionMethods[$this->reflectionType]['get' . $key])) { - return $this->{'get' . $key}(); - } - - if (isset(self::$reflectionMethods[$this->reflectionType]['is' . $key])) { - return $this->{'is' . $key}(); - } - - return $this->reflection->__get($name); - } - - /** - * Checks if the given property exists. - * - * First tries the envelope object's property storage, then its methods - * and finally the inspected element reflection. - * - * @param mixed $name Property name - * @return boolean - */ - public function __isset($name) - { - $key = ucfirst($name); - return isset(self::$reflectionMethods[$this->reflectionType]['get' . $key]) || isset(self::$reflectionMethods[$this->reflectionType]['is' . $key]) || $this->reflection->__isset($name); - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->reflection->getBroker(); - } - - /** - * Returns the name (FQN). - * - * @return string - */ - public function getName() - { - return $this->reflection->getName(); - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->reflection->getPrettyName(); - } - - /** - * Returns if the reflection object is internal. - * - * @return boolean - */ - public function isInternal() - { - return $this->reflection->isInternal(); - } - - /** - * Returns if the reflection object is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return $this->reflection->isUserDefined(); - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return $this->reflection->isTokenized(); - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return string - */ - public function getFileName() - { - return $this->reflection->getFileName(); - } - - /** - * Returns the definition start line number in the file. - * - * @return integer - */ - public function getStartLine() - { - $startLine = $this->reflection->getStartLine(); - - if ($doc = $this->getDocComment()) { - $startLine -= substr_count($doc, "\n") + 1; - } - - return $startLine; - } - - /** - * Returns the definition end line number in the file. - * - * @return integer - */ - public function getEndLine() - { - return $this->reflection->getEndLine(); - } -} diff --git a/apigen/ApiGen/ReflectionClass.php b/apigen/ApiGen/ReflectionClass.php deleted file mode 100644 index 2dd2c377cff..00000000000 --- a/apigen/ApiGen/ReflectionClass.php +++ /dev/null @@ -1,1423 +0,0 @@ -accessLevels) < 3) { - self::$methodAccessLevels = 0; - self::$propertyAccessLevels = 0; - - foreach (self::$config->accessLevels as $level) { - switch (strtolower($level)) { - case 'public': - self::$methodAccessLevels |= InternalReflectionMethod::IS_PUBLIC; - self::$propertyAccessLevels |= InternalReflectionProperty::IS_PUBLIC; - break; - case 'protected': - self::$methodAccessLevels |= InternalReflectionMethod::IS_PROTECTED; - self::$propertyAccessLevels |= InternalReflectionProperty::IS_PROTECTED; - break; - case 'private': - self::$methodAccessLevels |= InternalReflectionMethod::IS_PRIVATE; - self::$propertyAccessLevels |= InternalReflectionProperty::IS_PRIVATE; - break; - default: - break; - } - } - } else { - self::$methodAccessLevels = null; - self::$propertyAccessLevels = null; - } - } - } - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - return $this->reflection->getShortName(); - } - - /** - * Returns modifiers. - * - * @return array - */ - public function getModifiers() - { - return $this->reflection->getModifiers(); - } - - /** - * Returns if the class is abstract. - * - * @return boolean - */ - public function isAbstract() - { - return $this->reflection->isAbstract(); - } - - /** - * Returns if the class is final. - * - * @return boolean - */ - public function isFinal() - { - return $this->reflection->isFinal(); - } - - /** - * Returns if the class is an interface. - * - * @return boolean - */ - public function isInterface() - { - return $this->reflection->isInterface(); - } - - /** - * Returns if the class is an exception or its descendant. - * - * @return boolean - */ - public function isException() - { - return $this->reflection->isException(); - } - - /** - * Returns if the current class is a subclass of the given class. - * - * @param string $class Class name - * @return boolean - */ - public function isSubclassOf($class) - { - return $this->reflection->isSubclassOf($class); - } - - /** - * Returns visible methods. - * - * @return array - */ - public function getMethods() - { - if (null === $this->methods) { - $this->methods = $this->getOwnMethods(); - foreach ($this->reflection->getMethods(self::$methodAccessLevels) as $method) { - if (isset($this->methods[$method->getName()])) { - continue; - } - $apiMethod = new ReflectionMethod($method, self::$generator); - if (!$this->isDocumented() || $apiMethod->isDocumented()) { - $this->methods[$method->getName()] = $apiMethod; - } - } - } - return $this->methods; - } - - /** - * Returns visible methods declared by inspected class. - * - * @return array - */ - public function getOwnMethods() - { - if (null === $this->ownMethods) { - $this->ownMethods = array(); - foreach ($this->reflection->getOwnMethods(self::$methodAccessLevels) as $method) { - $apiMethod = new ReflectionMethod($method, self::$generator); - if (!$this->isDocumented() || $apiMethod->isDocumented()) { - $this->ownMethods[$method->getName()] = $apiMethod; - } - } - } - return $this->ownMethods; - } - - /** - * Returns visible magic methods. - * - * @return array - */ - public function getMagicMethods() - { - $methods = $this->getOwnMagicMethods(); - - $parent = $this->getParentClass(); - while ($parent) { - foreach ($parent->getOwnMagicMethods() as $method) { - if (isset($methods[$method->getName()])) { - continue; - } - - if (!$this->isDocumented() || $method->isDocumented()) { - $methods[$method->getName()] = $method; - } - } - $parent = $parent->getParentClass(); - } - - foreach ($this->getTraits() as $trait) { - foreach ($trait->getOwnMagicMethods() as $method) { - if (isset($methods[$method->getName()])) { - continue; - } - - if (!$this->isDocumented() || $method->isDocumented()) { - $methods[$method->getName()] = $method; - } - } - } - - return $methods; - } - - /** - * Returns visible magic methods declared by inspected class. - * - * @return array - */ - public function getOwnMagicMethods() - { - if (null === $this->ownMagicMethods) { - $this->ownMagicMethods = array(); - - if (!(self::$methodAccessLevels & InternalReflectionMethod::IS_PUBLIC) || false === $this->getDocComment()) { - return $this->ownMagicMethods; - } - - $annotations = $this->getAnnotation('method'); - if (null === $annotations) { - return $this->ownMagicMethods; - } - - foreach ($annotations as $annotation) { - if (!preg_match('~^(?:([\\w\\\\]+(?:\\|[\\w\\\\]+)*)\\s+)?(&)?\\s*(\\w+)\\s*\\(\\s*(.*)\\s*\\)\\s*(.*|$)~s', $annotation, $matches)) { - // Wrong annotation format - continue; - } - - list(, $returnTypeHint, $returnsReference, $name, $args, $shortDescription) = $matches; - - $doc = $this->getDocComment(); - $tmp = $annotation; - if ($delimiter = strpos($annotation, "\n")) { - $tmp = substr($annotation, 0, $delimiter); - } - - $startLine = $this->getStartLine() + substr_count(substr($doc, 0, strpos($doc, $tmp)), "\n"); - $endLine = $startLine + substr_count($annotation, "\n"); - - $method = new ReflectionMethodMagic(null, self::$generator); - $method - ->setName($name) - ->setShortDescription(str_replace("\n", ' ', $shortDescription)) - ->setStartLine($startLine) - ->setEndLine($endLine) - ->setReturnsReference('&' === $returnsReference) - ->setDeclaringClass($this) - ->addAnnotation('return', $returnTypeHint); - - $this->ownMagicMethods[$name] = $method; - - $parameters = array(); - foreach (array_filter(preg_split('~\\s*,\\s*~', $args)) as $position => $arg) { - if (!preg_match('~^(?:([\\w\\\\]+(?:\\|[\\w\\\\]+)*)\\s+)?(&)?\\s*\\$(\\w+)(?:\\s*=\\s*(.*))?($)~s', $arg, $matches)) { - // Wrong annotation format - continue; - } - - list(, $typeHint, $passedByReference, $name, $defaultValueDefinition) = $matches; - - if (empty($typeHint)) { - $typeHint = 'mixed'; - } - - $parameter = new ReflectionParameterMagic(null, self::$generator); - $parameter - ->setName($name) - ->setPosition($position) - ->setTypeHint($typeHint) - ->setDefaultValueDefinition($defaultValueDefinition) - ->setUnlimited(false) - ->setPassedByReference('&' === $passedByReference) - ->setDeclaringFunction($method); - - $parameters[$name] = $parameter; - - $method->addAnnotation('param', ltrim(sprintf('%s $%s', $typeHint, $name))); - } - $method->setParameters($parameters); - } - } - return $this->ownMagicMethods; - } - - /** - * Returns visible methods declared by traits. - * - * @return array - */ - public function getTraitMethods() - { - $methods = array(); - foreach ($this->reflection->getTraitMethods(self::$methodAccessLevels) as $method) { - $apiMethod = new ReflectionMethod($method, self::$generator); - if (!$this->isDocumented() || $apiMethod->isDocumented()) { - $methods[$method->getName()] = $apiMethod; - } - } - return $methods; - } - - /** - * Returns a method reflection. - * - * @param string $name Method name - * @return \ApiGen\ReflectionMethod - * @throws \InvalidArgumentException If required method does not exist. - */ - public function getMethod($name) - { - if ($this->hasMethod($name)) { - return $this->methods[$name]; - } - - throw new InvalidArgumentException(sprintf('Method %s does not exist in class %s', $name, $this->reflection->getName())); - } - - /** - * Returns visible properties. - * - * @return array - */ - public function getProperties() - { - if (null === $this->properties) { - $this->properties = $this->getOwnProperties(); - foreach ($this->reflection->getProperties(self::$propertyAccessLevels) as $property) { - if (isset($this->properties[$property->getName()])) { - continue; - } - $apiProperty = new ReflectionProperty($property, self::$generator); - if (!$this->isDocumented() || $apiProperty->isDocumented()) { - $this->properties[$property->getName()] = $apiProperty; - } - } - } - return $this->properties; - } - - /** - * Returns visible magic properties. - * - * @return array - */ - public function getMagicProperties() - { - $properties = $this->getOwnMagicProperties(); - - $parent = $this->getParentClass(); - while ($parent) { - foreach ($parent->getOwnMagicProperties() as $property) { - if (isset($properties[$method->getName()])) { - continue; - } - - if (!$this->isDocumented() || $property->isDocumented()) { - $properties[$property->getName()] = $property; - } - } - $parent = $parent->getParentClass(); - } - - foreach ($this->getTraits() as $trait) { - foreach ($trait->getOwnMagicProperties() as $property) { - if (isset($properties[$method->getName()])) { - continue; - } - - if (!$this->isDocumented() || $property->isDocumented()) { - $properties[$property->getName()] = $property; - } - } - } - - return $properties; - } - - /** - * Returns visible properties declared by inspected class. - * - * @return array - */ - public function getOwnProperties() - { - if (null === $this->ownProperties) { - $this->ownProperties = array(); - foreach ($this->reflection->getOwnProperties(self::$propertyAccessLevels) as $property) { - $apiProperty = new ReflectionProperty($property, self::$generator); - if (!$this->isDocumented() || $apiProperty->isDocumented()) { - $this->ownProperties[$property->getName()] = $apiProperty; - } - } - } - return $this->ownProperties; - } - - /** - * Returns visible properties magicly declared by inspected class. - * - * @return array - */ - public function getOwnMagicProperties() - { - if (null === $this->ownMagicProperties) { - $this->ownMagicProperties = array(); - - if (!(self::$propertyAccessLevels & InternalReflectionProperty::IS_PUBLIC) || false === $this->getDocComment()) { - return $this->ownMagicProperties; - } - - foreach (array('property', 'property-read', 'property-write') as $annotationName) { - $annotations = $this->getAnnotation($annotationName); - if (null === $annotations) { - continue; - } - - foreach ($annotations as $annotation) { - if (!preg_match('~^(?:([\\w\\\\]+(?:\\|[\\w\\\\]+)*)\\s+)?\\$(\\w+)(?:\\s+(.*))?($)~s', $annotation, $matches)) { - // Wrong annotation format - continue; - } - - list(, $typeHint, $name, $shortDescription) = $matches; - - if (empty($typeHint)) { - $typeHint = 'mixed'; - } - - $doc = $this->getDocComment(); - $tmp = $annotation; - if ($delimiter = strpos($annotation, "\n")) { - $tmp = substr($annotation, 0, $delimiter); - } - - $startLine = $this->getStartLine() + substr_count(substr($doc, 0, strpos($doc, $tmp)), "\n"); - $endLine = $startLine + substr_count($annotation, "\n"); - - $magicProperty = new ReflectionPropertyMagic(null, self::$generator); - $magicProperty - ->setName($name) - ->setTypeHint($typeHint) - ->setShortDescription(str_replace("\n", ' ', $shortDescription)) - ->setStartLine($startLine) - ->setEndLine($endLine) - ->setReadOnly('property-read' === $annotationName) - ->setWriteOnly('property-write' === $annotationName) - ->setDeclaringClass($this) - ->addAnnotation('var', $typeHint); - - $this->ownMagicProperties[$name] = $magicProperty; - } - } - } - - return $this->ownMagicProperties; - } - - /** - * Returns visible properties declared by traits. - * - * @return array - */ - public function getTraitProperties() - { - $properties = array(); - foreach ($this->reflection->getTraitProperties(self::$propertyAccessLevels) as $property) { - $apiProperty = new ReflectionProperty($property, self::$generator); - if (!$this->isDocumented() || $apiProperty->isDocumented()) { - $properties[$property->getName()] = $apiProperty; - } - } - return $properties; - } - - /** - * Returns a method property. - * - * @param string $name Method name - * @return \ApiGen\ReflectionProperty - * @throws \InvalidArgumentException If required property does not exist. - */ - public function getProperty($name) - { - if ($this->hasProperty($name)) { - return $this->properties[$name]; - } - - throw new InvalidArgumentException(sprintf('Property %s does not exist in class %s', $name, $this->reflection->getName())); - } - - /** - * Returns visible properties. - * - * @return array - */ - public function getConstants() - { - if (null === $this->constants) { - $this->constants = array(); - foreach ($this->reflection->getConstantReflections() as $constant) { - $apiConstant = new ReflectionConstant($constant, self::$generator); - if (!$this->isDocumented() || $apiConstant->isDocumented()) { - $this->constants[$constant->getName()] = $apiConstant; - } - } - } - - return $this->constants; - } - - /** - * Returns constants declared by inspected class. - * - * @return array - */ - public function getOwnConstants() - { - if (null === $this->ownConstants) { - $this->ownConstants = array(); - $className = $this->reflection->getName(); - foreach ($this->getConstants() as $constantName => $constant) { - if ($className === $constant->getDeclaringClassName()) { - $this->ownConstants[$constantName] = $constant; - } - } - } - return $this->ownConstants; - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \ApiGen\ReflectionConstant - * @throws \InvalidArgumentException If required constant does not exist. - */ - public function getConstantReflection($name) - { - if (null === $this->constants) { - $this->getConstants(); - } - - if (isset($this->constants[$name])) { - return $this->constants[$name]; - } - - throw new InvalidArgumentException(sprintf('Constant %s does not exist in class %s', $name, $this->reflection->getName())); - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \ApiGen\ReflectionConstant - */ - public function getConstant($name) - { - return $this->getConstantReflection($name); - } - - /** - * Checks if there is a constant of the given name. - * - * @param string $constantName Constant name - * @return boolean - */ - public function hasConstant($constantName) - { - if (null === $this->constants) { - $this->getConstants(); - } - - return isset($this->constants[$constantName]); - } - - /** - * Checks if there is a constant of the given name. - * - * @param string $constantName Constant name - * @return boolean - */ - public function hasOwnConstant($constantName) - { - if (null === $this->ownConstants) { - $this->getOwnConstants(); - } - - return isset($this->ownConstants[$constantName]); - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \ApiGen\ReflectionConstant - * @throws \InvalidArgumentException If required constant does not exist. - */ - public function getOwnConstantReflection($name) - { - if (null === $this->ownConstants) { - $this->getOwnConstants(); - } - - if (isset($this->ownConstants[$name])) { - return $this->ownConstants[$name]; - } - - throw new InvalidArgumentException(sprintf('Constant %s does not exist in class %s', $name, $this->reflection->getName())); - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \ApiGen\ReflectionConstant - */ - public function getOwnConstant($name) - { - return $this->getOwnConstantReflection($name); - } - - /** - * Returns a parent class reflection encapsulated by this class. - * - * @return \ApiGen\ReflectionClass - */ - public function getParentClass() - { - if ($className = $this->reflection->getParentClassName()) { - return self::$parsedClasses[$className]; - } - return $className; - } - - /** - * Returns the parent class name. - * - * @return string|null - */ - public function getParentClassName() - { - return $this->reflection->getParentClassName(); - } - - /** - * Returns all parent classes reflections encapsulated by this class. - * - * @return array - */ - public function getParentClasses() - { - if (null === $this->parentClasses) { - $classes = self::$parsedClasses; - $this->parentClasses = array_map(function(IReflectionClass $class) use ($classes) { - return $classes[$class->getName()]; - }, $this->reflection->getParentClasses()); - } - return $this->parentClasses; - } - - - /** - * Returns the parent classes names. - * - * @return array - */ - public function getParentClassNameList() - { - return $this->reflection->getParentClassNameList(); - } - - /** - * Returns if the class implements the given interface. - * - * @param string|object $interface Interface name or reflection object - * @return boolean - */ - public function implementsInterface($interface) - { - return $this->reflection->implementsInterface($interface); - } - - /** - * Returns all interface reflections encapsulated by this class. - * - * @return array - */ - public function getInterfaces() - { - $classes = self::$parsedClasses; - return array_map(function(IReflectionClass $class) use ($classes) { - return $classes[$class->getName()]; - }, $this->reflection->getInterfaces()); - } - - /** - * Returns interface names. - * - * @return array - */ - public function getInterfaceNames() - { - return $this->reflection->getInterfaceNames(); - } - - /** - * Returns all interfaces implemented by the inspected class and not its parents. - * - * @return array - */ - public function getOwnInterfaces() - { - $classes = self::$parsedClasses; - return array_map(function(IReflectionClass $class) use ($classes) { - return $classes[$class->getName()]; - }, $this->reflection->getOwnInterfaces()); - } - - /** - * Returns names of interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaceNames() - { - return $this->reflection->getOwnInterfaceNames(); - } - - /** - * Returns all traits reflections encapsulated by this class. - * - * @return array - */ - public function getTraits() - { - $classes = self::$parsedClasses; - return array_map(function(IReflectionClass $class) use ($classes) { - return $classes[$class->getName()]; - }, $this->reflection->getTraits()); - } - - /** - * Returns names of used traits. - * - * @return array - */ - public function getTraitNames() - { - return $this->reflection->getTraitNames(); - } - - /** - * Returns names of traits used by this class an not its parents. - * - * @return array - */ - public function getOwnTraitNames() - { - return $this->reflection->getOwnTraitNames(); - } - - /** - * Returns method aliases from traits. - * - * @return array - */ - public function getTraitAliases() - { - return $this->reflection->getTraitAliases(); - } - - /** - * Returns all traits used by the inspected class and not its parents. - * - * @return array - */ - public function getOwnTraits() - { - $classes = self::$parsedClasses; - return array_map(function(IReflectionClass $class) use ($classes) { - return $classes[$class->getName()]; - }, $this->reflection->getOwnTraits()); - } - - /** - * Returns if the class is a trait. - * - * @return boolean - */ - public function isTrait() - { - return $this->reflection->isTrait(); - } - - /** - * Returns if the class uses a particular trait. - * - * @param string $trait Trait name - * @return boolean - */ - public function usesTrait($trait) - { - return $this->reflection->usesTrait($trait); - } - - /** - * Returns reflections of direct subclasses. - * - * @return array - */ - public function getDirectSubClasses() - { - $subClasses = array(); - $name = $this->reflection->getName(); - foreach (self::$parsedClasses as $class) { - if (!$class->isDocumented()) { - continue; - } - if ($name === $class->getParentClassName()) { - $subClasses[] = $class; - } - } - return $subClasses; - } - - /** - * Returns reflections of indirect subclasses. - * - * @return array - */ - public function getIndirectSubClasses() - { - $subClasses = array(); - $name = $this->reflection->getName(); - foreach (self::$parsedClasses as $class) { - if (!$class->isDocumented()) { - continue; - } - if ($name !== $class->getParentClassName() && $class->isSubclassOf($name)) { - $subClasses[] = $class; - } - } - return $subClasses; - } - - /** - * Returns reflections of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementers() - { - if (!$this->isInterface()) { - return array(); - } - - $implementers = array(); - $name = $this->reflection->getName(); - foreach (self::$parsedClasses as $class) { - if (!$class->isDocumented()) { - continue; - } - if (in_array($name, $class->getOwnInterfaceNames())) { - $implementers[] = $class; - } - } - return $implementers; - } - - /** - * Returns reflections of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementers() - { - if (!$this->isInterface()) { - return array(); - } - - $implementers = array(); - $name = $this->reflection->getName(); - foreach (self::$parsedClasses as $class) { - if (!$class->isDocumented()) { - continue; - } - if ($class->implementsInterface($name) && !in_array($name, $class->getOwnInterfaceNames())) { - $implementers[] = $class; - } - } - return $implementers; - } - - /** - * Returns reflections of classes directly using this trait. - * - * @return array - */ - public function getDirectUsers() - { - if (!$this->isTrait()) { - return array(); - } - - $users = array(); - $name = $this->reflection->getName(); - foreach (self::$parsedClasses as $class) { - if (!$class->isDocumented()) { - continue; - } - - if (in_array($name, $class->getOwnTraitNames())) { - $users[] = $class; - } - } - return $users; - } - - /** - * Returns reflections of classes indirectly using this trait. - * - * @return array - */ - public function getIndirectUsers() - { - if (!$this->isTrait()) { - return array(); - } - - $users = array(); - $name = $this->reflection->getName(); - foreach (self::$parsedClasses as $class) { - if (!$class->isDocumented()) { - continue; - } - if ($class->usesTrait($name) && !in_array($name, $class->getOwnTraitNames())) { - $users[] = $class; - } - } - return $users; - } - - /** - * Returns an array of inherited methods from parent classes grouped by the declaring class name. - * - * @return array - */ - public function getInheritedMethods() - { - $methods = array(); - $allMethods = array_flip(array_map(function($method) { - return $method->getName(); - }, $this->getOwnMethods())); - - foreach (array_merge($this->getParentClasses(), $this->getInterfaces()) as $class) { - $inheritedMethods = array(); - foreach ($class->getOwnMethods() as $method) { - if (!array_key_exists($method->getName(), $allMethods) && !$method->isPrivate()) { - $inheritedMethods[$method->getName()] = $method; - $allMethods[$method->getName()] = null; - } - } - - if (!empty($inheritedMethods)) { - ksort($inheritedMethods); - $methods[$class->getName()] = array_values($inheritedMethods); - } - } - - return $methods; - } - - /** - * Returns an array of inherited magic methods from parent classes grouped by the declaring class name. - * - * @return array - */ - public function getInheritedMagicMethods() - { - $methods = array(); - $allMethods = array_flip(array_map(function($method) { - return $method->getName(); - }, $this->getOwnMagicMethods())); - - foreach (array_merge($this->getParentClasses(), $this->getInterfaces()) as $class) { - $inheritedMethods = array(); - foreach ($class->getOwnMagicMethods() as $method) { - if (!array_key_exists($method->getName(), $allMethods)) { - $inheritedMethods[$method->getName()] = $method; - $allMethods[$method->getName()] = null; - } - } - - if (!empty($inheritedMethods)) { - ksort($inheritedMethods); - $methods[$class->getName()] = array_values($inheritedMethods); - } - } - - return $methods; - } - - /** - * Returns an array of used methods from used traits grouped by the declaring trait name. - * - * @return array - */ - public function getUsedMethods() - { - $usedMethods = array(); - foreach ($this->getMethods() as $method) { - if (null === $method->getDeclaringTraitName() || $this->getName() === $method->getDeclaringTraitName()) { - continue; - } - - $usedMethods[$method->getDeclaringTraitName()][$method->getName()]['method'] = $method; - if (null !== $method->getOriginalName() && $method->getName() !== $method->getOriginalName()) { - $usedMethods[$method->getDeclaringTraitName()][$method->getName()]['aliases'][$method->getName()] = $method; - } - } - - // Sort - array_walk($usedMethods, function(&$methods) { - ksort($methods); - array_walk($methods, function(&$aliasedMethods) { - if (!isset($aliasedMethods['aliases'])) { - $aliasedMethods['aliases'] = array(); - } - ksort($aliasedMethods['aliases']); - }); - }); - - return $usedMethods; - } - - /** - * Returns an array of used magic methods from used traits grouped by the declaring trait name. - * - * @return array - */ - public function getUsedMagicMethods() - { - $usedMethods = array(); - - foreach ($this->getMagicMethods() as $method) { - if (null === $method->getDeclaringTraitName() || $this->getName() === $method->getDeclaringTraitName()) { - continue; - } - - $usedMethods[$method->getDeclaringTraitName()][$method->getName()]['method'] = $method; - } - - // Sort - array_walk($usedMethods, function(&$methods) { - ksort($methods); - array_walk($methods, function(&$aliasedMethods) { - if (!isset($aliasedMethods['aliases'])) { - $aliasedMethods['aliases'] = array(); - } - ksort($aliasedMethods['aliases']); - }); - }); - - return $usedMethods; - } - - /** - * Returns an array of inherited constants from parent classes grouped by the declaring class name. - * - * @return array - */ - public function getInheritedConstants() - { - return array_filter( - array_map( - function(ReflectionClass $class) { - $reflections = $class->getOwnConstants(); - ksort($reflections); - return $reflections; - }, - array_merge($this->getParentClasses(), $this->getInterfaces()) - ) - ); - } - - /** - * Returns an array of inherited properties from parent classes grouped by the declaring class name. - * - * @return array - */ - public function getInheritedProperties() - { - $properties = array(); - $allProperties = array_flip(array_map(function($property) { - return $property->getName(); - }, $this->getOwnProperties())); - - foreach ($this->getParentClasses() as $class) { - $inheritedProperties = array(); - foreach ($class->getOwnProperties() as $property) { - if (!array_key_exists($property->getName(), $allProperties) && !$property->isPrivate()) { - $inheritedProperties[$property->getName()] = $property; - $allProperties[$property->getName()] = null; - } - } - - if (!empty($inheritedProperties)) { - ksort($inheritedProperties); - $properties[$class->getName()] = array_values($inheritedProperties); - } - } - - return $properties; - } - - /** - * Returns an array of inherited magic properties from parent classes grouped by the declaring class name. - * - * @return array - */ - public function getInheritedMagicProperties() - { - $properties = array(); - $allProperties = array_flip(array_map(function($property) { - return $property->getName(); - }, $this->getOwnMagicProperties())); - - foreach ($this->getParentClasses() as $class) { - $inheritedProperties = array(); - foreach ($class->getOwnMagicProperties() as $property) { - if (!array_key_exists($property->getName(), $allProperties)) { - $inheritedProperties[$property->getName()] = $property; - $allProperties[$property->getName()] = null; - } - } - - if (!empty($inheritedProperties)) { - ksort($inheritedProperties); - $properties[$class->getName()] = array_values($inheritedProperties); - } - } - - return $properties; - } - - /** - * Returns an array of used properties from used traits grouped by the declaring trait name. - * - * @return array - */ - public function getUsedProperties() - { - $properties = array(); - $allProperties = array_flip(array_map(function($property) { - return $property->getName(); - }, $this->getOwnProperties())); - - foreach ($this->getTraits() as $trait) { - $usedProperties = array(); - foreach ($trait->getOwnProperties() as $property) { - if (!array_key_exists($property->getName(), $allProperties)) { - $usedProperties[$property->getName()] = $property; - $allProperties[$property->getName()] = null; - } - } - - if (!empty($usedProperties)) { - ksort($usedProperties); - $properties[$trait->getName()] = array_values($usedProperties); - } - } - - return $properties; - } - - /** - * Returns an array of used magic properties from used traits grouped by the declaring trait name. - * - * @return array - */ - public function getUsedMagicProperties() - { - $properties = array(); - $allProperties = array_flip(array_map(function($property) { - return $property->getName(); - }, $this->getOwnMagicProperties())); - - foreach ($this->getTraits() as $trait) { - $usedProperties = array(); - foreach ($trait->getOwnMagicProperties() as $property) { - if (!array_key_exists($property->getName(), $allProperties)) { - $usedProperties[$property->getName()] = $property; - $allProperties[$property->getName()] = null; - } - } - - if (!empty($usedProperties)) { - ksort($usedProperties); - $properties[$trait->getName()] = array_values($usedProperties); - } - } - - return $properties; - } - - /** - * Checks if there is a property of the given name. - * - * @param string $propertyName Property name - * @return boolean - */ - public function hasProperty($propertyName) - { - if (null === $this->properties) { - $this->getProperties(); - } - - return isset($this->properties[$propertyName]); - } - - /** - * Checks if there is a property of the given name. - * - * @param string $propertyName Property name - * @return boolean - */ - public function hasOwnProperty($propertyName) - { - if (null === $this->ownProperties) { - $this->getOwnProperties(); - } - - return isset($this->ownProperties[$propertyName]); - } - - /** - * Checks if there is a property of the given name. - * - * @param string $propertyName Property name - * @return boolean - */ - public function hasTraitProperty($propertyName) - { - $properties = $this->getTraitProperties(); - return isset($properties[$propertyName]); - } - - /** - * Checks if there is a method of the given name. - * - * @param string $methodName Method name - * @return boolean - */ - public function hasMethod($methodName) - { - if (null === $this->methods) { - $this->getMethods(); - } - - return isset($this->methods[$methodName]); - } - - /** - * Checks if there is a method of the given name. - * - * @param string $methodName Method name - * @return boolean - */ - public function hasOwnMethod($methodName) - { - if (null === $this->ownMethods) { - $this->getOwnMethods(); - } - - return isset($this->ownMethods[$methodName]); - } - - /** - * Checks if there is a method of the given name. - * - * @param string $methodName Method name - * @return boolean - */ - public function hasTraitMethod($methodName) - { - $methods = $this->getTraitMethods(); - return isset($methods[$methodName]); - } - - /** - * Returns if the class is valid. - * - * @return boolean - */ - public function isValid() - { - if ($this->reflection instanceof TokenReflection\Invalid\ReflectionClass) { - return false; - } - - return true; - } - - /** - * Returns if the class should be documented. - * - * @return boolean - */ - public function isDocumented() - { - if (null === $this->isDocumented && parent::isDocumented()) { - $fileName = self::$generator->unPharPath($this->reflection->getFilename()); - foreach (self::$config->skipDocPath as $mask) { - if (fnmatch($mask, $fileName, FNM_NOESCAPE)) { - $this->isDocumented = false; - break; - } - } - if (true === $this->isDocumented) { - foreach (self::$config->skipDocPrefix as $prefix) { - if (0 === strpos($this->reflection->getName(), $prefix)) { - $this->isDocumented = false; - break; - } - } - } - } - - return $this->isDocumented; - } -} diff --git a/apigen/ApiGen/ReflectionConstant.php b/apigen/ApiGen/ReflectionConstant.php deleted file mode 100644 index c7993aa39ce..00000000000 --- a/apigen/ApiGen/ReflectionConstant.php +++ /dev/null @@ -1,145 +0,0 @@ -reflection->getShortName(); - } - - /** - * Returns constant type hint. - * - * @return string - */ - public function getTypeHint() - { - if ($annotations = $this->getAnnotation('var')) { - list($types) = preg_split('~\s+|$~', $annotations[0], 2); - if (!empty($types)) { - return $types; - } - } - - try { - $type = gettype($this->getValue()); - if ('null' !== strtolower($type)) { - return $type; - } - } catch (\Exception $e) { - // Nothing - } - - return 'mixed'; - } - - /** - * Returns the constant declaring class. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringClass() - { - $className = $this->reflection->getDeclaringClassName(); - return null === $className ? null : self::$parsedClasses[$className]; - } - - /** - * Returns the name of the declaring class. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->reflection->getDeclaringClassName(); - } - - /** - * Returns the constant value. - * - * @return mixed - */ - public function getValue() - { - return $this->reflection->getValue(); - } - - /** - * Returns the constant value definition. - * - * @return string - */ - public function getValueDefinition() - { - return $this->reflection->getValueDefinition(); - } - - /** - * Returns if the constant is valid. - * - * @return boolean - */ - public function isValid() - { - if ($this->reflection instanceof \TokenReflection\Invalid\ReflectionConstant) { - return false; - } - - if ($class = $this->getDeclaringClass()) { - return $class->isValid(); - } - - return true; - } - - /** - * Returns if the constant should be documented. - * - * @return boolean - */ - public function isDocumented() - { - if (null === $this->isDocumented && parent::isDocumented() && null === $this->reflection->getDeclaringClassName()) { - $fileName = self::$generator->unPharPath($this->reflection->getFilename()); - foreach (self::$config->skipDocPath as $mask) { - if (fnmatch($mask, $fileName, FNM_NOESCAPE)) { - $this->isDocumented = false; - break; - } - } - if (true === $this->isDocumented) { - foreach (self::$config->skipDocPrefix as $prefix) { - if (0 === strpos($this->reflection->getName(), $prefix)) { - $this->isDocumented = false; - break; - } - } - } - } - - return $this->isDocumented; - } -} diff --git a/apigen/ApiGen/ReflectionElement.php b/apigen/ApiGen/ReflectionElement.php deleted file mode 100644 index 8f12485a3dd..00000000000 --- a/apigen/ApiGen/ReflectionElement.php +++ /dev/null @@ -1,373 +0,0 @@ -reflection->getExtension(); - return null === $extension ? null : new ReflectionExtension($extension, self::$generator); - } - - /** - * Returns the PHP extension name. - * - * @return boolean - */ - public function getExtensionName() - { - return $this->reflection->getExtensionName(); - } - - /** - * Returns the start position in the file token stream. - * - * @return integer - */ - public function getStartPosition() - { - return $this->reflection->getStartPosition(); - } - - /** - * Returns the end position in the file token stream. - * - * @return integer - */ - public function getEndPosition() - { - return $this->reflection->getEndPosition(); - } - - /** - * Returns if the element belongs to main project. - * - * @return boolean - */ - public function isMain() - { - return empty(self::$config->main) || 0 === strpos($this->getName(), self::$config->main); - } - - /** - * Returns if the element should be documented. - * - * @return boolean - */ - public function isDocumented() - { - if (null === $this->isDocumented) { - $this->isDocumented = $this->reflection->isTokenized() || $this->reflection->isInternal(); - - if ($this->isDocumented) { - if (!self::$config->php && $this->reflection->isInternal()) { - $this->isDocumented = false; - } elseif (!self::$config->deprecated && $this->reflection->isDeprecated()) { - $this->isDocumented = false; - } elseif (!self::$config->internal && ($internal = $this->reflection->getAnnotation('internal')) && empty($internal[0])) { - $this->isDocumented = false; - } elseif (count($this->reflection->getAnnotation('ignore')) > 0) { - $this->isDocumented = false; - } - } - } - - return $this->isDocumented; - } - - /** - * Returns if the element is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - if ($this->reflection->isDeprecated()) { - return true; - } - - if (($this instanceof ReflectionMethod || $this instanceof ReflectionProperty || $this instanceof ReflectionConstant) - && $class = $this->getDeclaringClass() - ) { - return $class->isDeprecated(); - } - - return false; - } - - /** - * Returns if the element is in package. - * - * @return boolean - */ - public function inPackage() - { - return '' !== $this->getPackageName(); - } - - /** - * Returns element package name (including subpackage name). - * - * @return string - */ - public function getPackageName() - { - static $packages = array(); - - if ($package = $this->getAnnotation('package')) { - $packageName = preg_replace('~\s+.*~s', '', $package[0]); - if (empty($packageName)) { - return ''; - } - - if ($subpackage = $this->getAnnotation('subpackage')) { - $subpackageName = preg_replace('~\s+.*~s', '', $subpackage[0]); - if (empty($subpackageName)) { - // Do nothing - } elseif (0 === strpos($subpackageName, $packageName)) { - $packageName = $subpackageName; - } else { - $packageName .= '\\' . $subpackageName; - } - } - $packageName = strtr($packageName, '._/', '\\\\\\'); - - $lowerPackageName = strtolower($packageName); - if (!isset($packages[$lowerPackageName])) { - $packages[$lowerPackageName] = $packageName; - } - - return $packages[$lowerPackageName]; - } - - return ''; - } - - /** - * Returns element package name (including subpackage name). - * - * For internal elements returns "PHP", for elements in global space returns "None". - * - * @return string - */ - public function getPseudoPackageName() - { - if ($this->isInternal()) { - return 'PHP'; - } - - return $this->getPackageName() ?: 'None'; - } - - /** - * Returns if the element is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return '' !== $this->getNamespaceName(); - } - - /** - * Returns element namespace name. - * - * @return string - */ - public function getNamespaceName() - { - static $namespaces = array(); - - $namespaceName = $this->reflection->getNamespaceName(); - - if (!$namespaceName) { - return $namespaceName; - } - - $lowerNamespaceName = strtolower($namespaceName); - if (!isset($namespaces[$lowerNamespaceName])) { - $namespaces[$lowerNamespaceName] = $namespaceName; - } - - return $namespaces[$lowerNamespaceName]; - } - - /** - * Returns element namespace name. - * - * For internal elements returns "PHP", for elements in global space returns "None". - * - * @return string - */ - public function getPseudoNamespaceName() - { - return $this->isInternal() ? 'PHP' : $this->getNamespaceName() ?: 'None'; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->reflection->getNamespaceAliases(); - } - - /** - * Returns the short description. - * - * @return string - */ - public function getShortDescription() - { - $short = $this->reflection->getAnnotation(\TokenReflection\ReflectionAnnotation::SHORT_DESCRIPTION); - if (!empty($short)) { - return $short; - } - - if ($this instanceof ReflectionProperty || $this instanceof ReflectionConstant) { - $var = $this->getAnnotation('var'); - list(, $short) = preg_split('~\s+|$~', $var[0], 2); - } - - return $short; - } - - /** - * Returns the long description. - * - * @return string - */ - public function getLongDescription() - { - $short = $this->getShortDescription(); - $long = $this->reflection->getAnnotation(\TokenReflection\ReflectionAnnotation::LONG_DESCRIPTION); - - if (!empty($long)) { - $short .= "\n\n" . $long; - } - - return $short; - } - - /** - * Returns the appropriate docblock definition. - * - * @return string|boolean - */ - public function getDocComment() - { - return $this->reflection->getDocComment(); - } - - /** - * Returns reflection element annotations. - * - * Removes the short and long description. - * - * In case of classes, functions and constants, @package, @subpackage, @author and @license annotations - * are added from declaring files if not already present. - * - * @return array - */ - public function getAnnotations() - { - if (null === $this->annotations) { - static $fileLevel = array('package' => true, 'subpackage' => true, 'author' => true, 'license' => true, 'copyright' => true); - - $annotations = $this->reflection->getAnnotations(); - unset($annotations[\TokenReflection\ReflectionAnnotation::SHORT_DESCRIPTION]); - unset($annotations[\TokenReflection\ReflectionAnnotation::LONG_DESCRIPTION]); - - if ($this->reflection instanceof \TokenReflection\ReflectionClass || $this->reflection instanceof \TokenReflection\ReflectionFunction || ($this->reflection instanceof \TokenReflection\ReflectionConstant && null === $this->reflection->getDeclaringClassName())) { - foreach ($this->reflection->getFileReflection()->getAnnotations() as $name => $value) { - if (isset($fileLevel[$name]) && empty($annotations[$name])) { - $annotations[$name] = $value; - } - } - } - - $this->annotations = $annotations; - } - - return $this->annotations; - } - - /** - * Returns reflection element annotation. - * - * @param string $annotation Annotation name - * @return array - */ - public function getAnnotation($annotation) - { - $annotations = $this->getAnnotations(); - return isset($annotations[$annotation]) ? $annotations[$annotation] : null; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $annotation Annotation name - * @return boolean - */ - public function hasAnnotation($annotation) - { - $annotations = $this->getAnnotations(); - return isset($annotations[$annotation]); - } - - /** - * Adds element annotation. - * - * @param string $annotation Annotation name - * @param string $value Annotation value - * @return \ApiGen\ReflectionElement - */ - public function addAnnotation($annotation, $value) - { - if (null === $this->annotations) { - $this->getAnnotations(); - } - $this->annotations[$annotation][] = $value; - - return $this; - } -} diff --git a/apigen/ApiGen/ReflectionExtension.php b/apigen/ApiGen/ReflectionExtension.php deleted file mode 100644 index 1d636bcdaff..00000000000 --- a/apigen/ApiGen/ReflectionExtension.php +++ /dev/null @@ -1,135 +0,0 @@ -reflection->getClass($name); - if (null === $class) { - return null; - } - if (isset(self::$parsedClasses[$name])) { - return self::$parsedClasses[$name]; - } - return new ReflectionClass($class, self::$generator); - } - - /** - * Returns classes defined by this extension. - * - * @return array - */ - public function getClasses() - { - $generator = self::$generator; - $classes = self::$parsedClasses; - return array_map(function(TokenReflection\IReflectionClass $class) use ($generator, $classes) { - return isset($classes[$class->getName()]) ? $classes[$class->getName()] : new ReflectionClass($class, $generator); - }, $this->reflection->getClasses()); - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \ApiGen\ReflectionConstant|null - */ - public function getConstant($name) - { - return $this->getConstantReflection($name); - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \ApiGen\ReflectionConstant|null - */ - public function getConstantReflection($name) - { - $constant = $this->reflection->getConstantReflection($name); - return null === $constant ? null : new ReflectionConstant($constant, self::$generator); - } - - /** - * Returns reflections of defined constants. - * - * @return array - */ - public function getConstants() - { - return $this->getConstantReflections(); - } - - /** - * Returns reflections of defined constants. - * - * @return array - */ - public function getConstantReflections() - { - $generator = self::$generator; - return array_map(function(TokenReflection\IReflectionConstant $constant) use ($generator) { - return new ReflectionConstant($constant, $generator); - }, $this->reflection->getConstantReflections()); - } - - /** - * Returns a function reflection. - * - * @param string $name Function name - * @return \ApiGen\ReflectionFunction - */ - public function getFunction($name) - { - $function = $this->reflection->getFunction($name); - return null === $function ? null : new ReflectionFunction($function, self::$generator); - } - - /** - * Returns functions defined by this extension. - * - * @return array - */ - public function getFunctions() - { - $generator = self::$generator; - return array_map(function(TokenReflection\IReflectionFunction $function) use ($generator) { - return new ReflectionFunction($function, $generator); - }, $this->reflection->getFunctions()); - } - - /** - * Returns names of functions defined by this extension. - * - * @return array - */ - public function getFunctionNames() - { - return $this->reflection->getFunctionNames(); - } -} diff --git a/apigen/ApiGen/ReflectionFunction.php b/apigen/ApiGen/ReflectionFunction.php deleted file mode 100644 index 75f2e4038d8..00000000000 --- a/apigen/ApiGen/ReflectionFunction.php +++ /dev/null @@ -1,64 +0,0 @@ -reflection instanceof \TokenReflection\Invalid\ReflectionFunction) { - return false; - } - - return true; - } - - /** - * Returns if the function should be documented. - * - * @return boolean - */ - public function isDocumented() - { - if (null === $this->isDocumented && parent::isDocumented()) { - $fileName = self::$generator->unPharPath($this->reflection->getFilename()); - foreach (self::$config->skipDocPath as $mask) { - if (fnmatch($mask, $fileName, FNM_NOESCAPE)) { - $this->isDocumented = false; - break; - } - } - if (true === $this->isDocumented) { - foreach (self::$config->skipDocPrefix as $prefix) { - if (0 === strpos($this->reflection->getName(), $prefix)) { - $this->isDocumented = false; - break; - } - } - } - } - - return $this->isDocumented; - } -} diff --git a/apigen/ApiGen/ReflectionFunctionBase.php b/apigen/ApiGen/ReflectionFunctionBase.php deleted file mode 100644 index a86326fd9af..00000000000 --- a/apigen/ApiGen/ReflectionFunctionBase.php +++ /dev/null @@ -1,151 +0,0 @@ -reflection->getShortName(); - } - - /** - * Returns if the function/method returns its value as reference. - * - * @return boolean - */ - public function returnsReference() - { - return $this->reflection->returnsReference(); - } - - /** - * Returns a list of function/method parameters. - * - * @return array - */ - public function getParameters() - { - if (null === $this->parameters) { - $generator = self::$generator; - $this->parameters = array_map(function(TokenReflection\IReflectionParameter $parameter) use ($generator) { - return new ReflectionParameter($parameter, $generator); - }, $this->reflection->getParameters()); - - $annotations = $this->getAnnotation('param'); - if (null !== $annotations) { - foreach ($annotations as $position => $annotation) { - if (isset($parameters[$position])) { - // Standard parameter - continue; - } - - if (!preg_match('~^(?:([\\w\\\\]+(?:\\|[\\w\\\\]+)*)\\s+)?\\$(\\w+),\\.{3}(?:\\s+(.*))?($)~s', $annotation, $matches)) { - // Wrong annotation format - continue; - } - - list(, $typeHint, $name) = $matches; - - if (empty($typeHint)) { - $typeHint = 'mixed'; - } - - $parameter = new ReflectionParameterMagic(null, self::$generator); - $parameter - ->setName($name) - ->setPosition($position) - ->setTypeHint($typeHint) - ->setDefaultValueDefinition(null) - ->setUnlimited(true) - ->setPassedByReference(false) - ->setDeclaringFunction($this); - - $this->parameters[$position] = $parameter; - } - } - } - - return $this->parameters; - } - - /** - * Returns a particular function/method parameter. - * - * @param integer|string $parameterName Parameter name or position - * @return \ApiGen\ReflectionParameter - * @throws \InvalidArgumentException If there is no parameter of the given name. - * @throws \InvalidArgumentException If there is no parameter at the given position. - */ - public function getParameter($parameterName) - { - $parameters = $this->getParameters(); - - if (is_numeric($parameterName)) { - if (isset($parameters[$parameterName])) { - return $parameters[$parameterName]; - } - - throw new InvalidArgumentException(sprintf('There is no parameter at position "%d" in function/method "%s"', $parameterName, $this->getName()), Exception\Runtime::DOES_NOT_EXIST); - } else { - foreach ($parameters as $parameter) { - if ($parameter->getName() === $parameterName) { - return $parameter; - } - } - - throw new InvalidArgumentException(sprintf('There is no parameter "%s" in function/method "%s"', $parameterName, $this->getName()), Exception\Runtime::DOES_NOT_EXIST); - } - } - - /** - * Returns the number of parameters. - * - * @return integer - */ - public function getNumberOfParameters() - { - return $this->reflection->getNumberOfParameters(); - } - - /** - * Returns the number of required parameters. - * - * @return integer - */ - public function getNumberOfRequiredParameters() - { - return $this->reflection->getNumberOfRequiredParameters(); - } -} diff --git a/apigen/ApiGen/ReflectionMethod.php b/apigen/ApiGen/ReflectionMethod.php deleted file mode 100644 index 007935ccd9f..00000000000 --- a/apigen/ApiGen/ReflectionMethod.php +++ /dev/null @@ -1,250 +0,0 @@ -reflection->getDeclaringClassName(); - return null === $className ? null : self::$parsedClasses[$className]; - } - - /** - * Returns the declaring class name. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->reflection->getDeclaringClassName(); - } - - /** - * Returns method modifiers. - * - * @return integer - */ - public function getModifiers() - { - return $this->reflection->getModifiers(); - } - - /** - * Returns if the method is abstract. - * - * @return boolean - */ - public function isAbstract() - { - return $this->reflection->isAbstract(); - } - - /** - * Returns if the method is final. - * - * @return boolean - */ - public function isFinal() - { - return $this->reflection->isFinal(); - } - - /** - * Returns if the method is private. - * - * @return boolean - */ - public function isPrivate() - { - return $this->reflection->isPrivate(); - } - - /** - * Returns if the method is protected. - * - * @return boolean - */ - public function isProtected() - { - return $this->reflection->isProtected(); - } - - /** - * Returns if the method is public. - * - * @return boolean - */ - public function isPublic() - { - return $this->reflection->isPublic(); - } - - /** - * Returns if the method is static. - * - * @return boolean - */ - public function isStatic() - { - return $this->reflection->isStatic(); - } - - /** - * Returns if the method is a constructor. - * - * @return boolean - */ - public function isConstructor() - { - return $this->reflection->isConstructor(); - } - - /** - * Returns if the method is a destructor. - * - * @return boolean - */ - public function isDestructor() - { - return $this->reflection->isDestructor(); - } - - /** - * Returns the method declaring trait. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringTrait() - { - $traitName = $this->reflection->getDeclaringTraitName(); - return null === $traitName ? null : self::$parsedClasses[$traitName]; - } - - /** - * Returns the declaring trait name. - * - * @return string|null - */ - public function getDeclaringTraitName() - { - return $this->reflection->getDeclaringTraitName(); - } - - /** - * Returns the overridden method. - * - * @return \ApiGen\ReflectionMethod|null - */ - public function getImplementedMethod() - { - foreach ($this->getDeclaringClass()->getOwnInterfaces() as $interface) { - if ($interface->hasMethod($this->getName())) { - return $interface->getMethod($this->getName()); - } - } - - return null; - } - - /** - * Returns the overridden method. - * - * @return \ApiGen\ReflectionMethod|null - */ - public function getOverriddenMethod() - { - $parent = $this->getDeclaringClass()->getParentClass(); - if (null === $parent) { - return null; - } - - foreach ($parent->getMethods() as $method) { - if ($this->getName() === $method->getName()) { - if (!$method->isPrivate() && !$method->isAbstract()) { - return $method; - } else { - return null; - } - } - } - - return null; - } - - /** - * Returns the original name when importing from a trait. - * - * @return string|null - */ - public function getOriginalName() - { - return $this->reflection->getOriginalName(); - } - - /** - * Returns the original modifiers value when importing from a trait. - * - * @return integer|null - */ - public function getOriginalModifiers() - { - return $this->reflection->getOriginalModifiers(); - } - - /** - * Returns the original method when importing from a trait. - * - * @return \ApiGen\ReflectionMethod|null - */ - public function getOriginal() - { - $originalName = $this->reflection->getOriginalName(); - return null === $originalName ? null : self::$parsedClasses[$this->reflection->getOriginal()->getDeclaringClassName()]->getMethod($originalName); - } - - /** - * Returns if the method is valid. - * - * @return boolean - */ - public function isValid() - { - if ($class = $this->getDeclaringClass()) { - return $class->isValid(); - } - - return true; - } -} diff --git a/apigen/ApiGen/ReflectionMethodMagic.php b/apigen/ApiGen/ReflectionMethodMagic.php deleted file mode 100644 index 05b4514f746..00000000000 --- a/apigen/ApiGen/ReflectionMethodMagic.php +++ /dev/null @@ -1,729 +0,0 @@ -reflectionType = get_class($this); - if (!isset(self::$reflectionMethods[$this->reflectionType])) { - self::$reflectionMethods[$this->reflectionType] = array_flip(get_class_methods($this)); - } - } - - /** - * Sets method name. - * - * @param string $name - * @return \Apigen\ReflectionMethodMagic - */ - public function setName($name) - { - $this->name = (string) $name; - return $this; - - } - - /** - * Sets short description. - * - * @param string $shortDescription - * @return \Apigen\ReflectionMethodMagic - */ - public function setShortDescription($shortDescription) - { - $this->shortDescription = (string) $shortDescription; - return $this; - } - - /** - * Sets start line. - * - * @param integer $startLine - * @return \Apigen\ReflectionMethodMagic - */ - public function setStartLine($startLine) - { - $this->startLine = (int) $startLine; - return $this; - } - - /** - * Sets end line. - * - * @param integer $endLine - * @return \Apigen\ReflectionMethodMagic - */ - public function setEndLine($endLine) - { - $this->endLine = (int) $endLine; - return $this; - } - - /** - * Sets if the method returns reference. - * - * @param boolean $returnsReference - * @return \Apigen\ReflectionMethodMagic - */ - public function setReturnsReference($returnsReference) - { - $this->returnsReference = (bool) $returnsReference; - return $this; - } - - /** - * Sets parameters. - * - * @param array $parameters - * @return \Apigen\ReflectionMethodMagic - */ - public function setParameters(array $parameters) - { - $this->parameters = $parameters; - return $this; - } - - /** - * Sets declaring class. - * - * @param \ApiGen\ReflectionClass $declaringClass - * @return \ApiGen\ReflectionMethodMagic - */ - public function setDeclaringClass(ReflectionClass $declaringClass) - { - $this->declaringClass = $declaringClass; - return $this; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->declaringClass->getBroker(); - } - - /** - * Returns the start position in the file token stream. - * - * @return integer - */ - public function getStartPosition() - { - return $this->declaringClass->getStartPosition(); - } - - /** - * Returns the end position in the file token stream. - * - * @return integer - */ - public function getEndPosition() - { - return $this->declaringClass->getEndPosition(); - } - - /** - * Returns the name. - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the short description. - * - * @return string - */ - public function getShortDescription() - { - return $this->shortDescription; - } - - /** - * Returns the long description. - * - * @return string - */ - public function getLongDescription() - { - return $this->shortDescription; - } - - /** - * Returns the definition start line number in the file. - * - * @return integer - */ - public function getStartLine() - { - return $this->startLine; - } - - /** - * Returns the definition end line number in the file. - * - * @return integer - */ - public function getEndLine() - { - return $this->endLine; - } - - /** - * Returns if the function/method returns its value as reference. - * - * @return boolean - */ - public function returnsReference() - { - return $this->returnsReference; - } - - /** - * Returns if the property is magic. - * - * @return boolean - */ - public function isMagic() - { - return true; - } - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - return $this->name; - } - - /** - * Returns the PHP extension reflection. - * - * @return \ApiGen\ReflectionExtension|null - */ - public function getExtension() - { - return null; - } - - /** - * Returns the PHP extension name. - * - * @return boolean - */ - public function getExtensionName() - { - return false; - } - - /** - * Returns if the method should be documented. - * - * @return boolean - */ - public function isDocumented() - { - if (null === $this->isDocumented) { - $this->isDocumented = self::$config->deprecated || !$this->isDeprecated(); - } - - return $this->isDocumented; - } - - /** - * Returns if the property is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return $this->declaringClass->isDeprecated(); - } - - /** - * Returns property package name (including subpackage name). - * - * @return string - */ - public function getPackageName() - { - return $this->declaringClass->getPackageName(); - } - - /** - * Returns property namespace name. - * - * @return string - */ - public function getNamespaceName() - { - return $this->declaringClass->getNamespaceName(); - } - - /** - * Returns property annotations. - * - * @return array - */ - public function getAnnotations() - { - if (null === $this->annotations) { - $this->annotations = array(); - } - return $this->annotations; - } - - /** - * Returns the method declaring class. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringClass() - { - return $this->declaringClass; - } - - /** - * Returns the declaring class name. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->declaringClass->getName(); - } - - /** - * Returns method modifiers. - * - * @return integer - */ - public function getModifiers() - { - return InternalReflectionMethod::IS_PUBLIC; - } - - /** - * Returns if the method is abstract. - * - * @return boolean - */ - public function isAbstract() - { - return false; - } - - /** - * Returns if the method is final. - * - * @return boolean - */ - public function isFinal() - { - return false; - } - - /** - * Returns if the method is private. - * - * @return boolean - */ - public function isPrivate() - { - return false; - } - - /** - * Returns if the method is protected. - * - * @return boolean - */ - public function isProtected() - { - return false; - } - - /** - * Returns if the method is public. - * - * @return boolean - */ - public function isPublic() - { - return true; - } - - /** - * Returns if the method is static. - * - * @return boolean - */ - public function isStatic() - { - return false; - } - - /** - * Returns if the property is internal. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns if the method is a constructor. - * - * @return boolean - */ - public function isConstructor() - { - return false; - } - - /** - * Returns if the method is a destructor. - * - * @return boolean - */ - public function isDestructor() - { - return false; - } - - /** - * Returns the method declaring trait. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringTrait() - { - return $this->declaringClass->isTrait() ? $this->declaringClass : null; - } - - /** - * Returns the declaring trait name. - * - * @return string|null - */ - public function getDeclaringTraitName() - { - if ($declaringTrait = $this->getDeclaringTrait()) { - return $declaringTrait->getName(); - } - return null; - } - - /** - * Returns the overridden method. - * - * @return \ApiGen\ReflectionMethod|null - */ - public function getImplementedMethod() - { - return null; - } - - /** - * Returns the overridden method. - * - * @return \ApiGen\ReflectionMethod|null - */ - public function getOverriddenMethod() - { - $parent = $this->declaringClass->getParentClass(); - if (null === $parent) { - return null; - } - - foreach ($parent->getMagicMethods() as $method) { - if ($this->name === $method->getName()) { - return $method; - } - } - - return null; - } - - /** - * Returns the original name when importing from a trait. - * - * @return string|null - */ - public function getOriginalName() - { - return $this->getName(); - } - - /** - * Returns the original modifiers value when importing from a trait. - * - * @return integer|null - */ - public function getOriginalModifiers() - { - return $this->getModifiers(); - } - - /** - * Returns the original method when importing from a trait. - * - * @return \ApiGen\ReflectionMethod|null - */ - public function getOriginal() - { - return null; - } - - /** - * Returns a list of method parameters. - * - * @return array - */ - public function getParameters() - { - return $this->parameters; - } - - /** - * Returns the number of parameters. - * - * @return integer - */ - public function getNumberOfParameters() - { - return count($this->parameters); - } - - /** - * Returns the number of required parameters. - * - * @return integer - */ - public function getNumberOfRequiredParameters() - { - $count = 0; - array_walk($this->parameters, function(ReflectionParameter $parameter) use (&$count) { - if (!$parameter->isOptional()) { - $count++; - } - }); - return $count; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->declaringClass->getNamespaceAliases(); - } - - /** - * Returns an property pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return sprintf('%s::%s()', $this->declaringClass->getName(), $this->name); - } - - /** - * Returns the file name the method is defined in. - * - * @return string - */ - public function getFileName() - { - return $this->declaringClass->getFileName(); - } - - /** - * Returns if the method is user defined. - - * @return boolean - */ - public function isUserDefined() - { - return true; - } - - /** - * Returns if the method comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return true; - } - - /** - * Returns the appropriate docblock definition. - * - * @return string|boolean - */ - public function getDocComment() - { - $docComment = "/**\n"; - - if (!empty($this->shortDescription)) { - $docComment .= $this->shortDescription . "\n\n"; - } - - if ($annotations = $this->getAnnotation('param')) { - foreach ($annotations as $annotation) { - $docComment .= sprintf("@param %s\n", $annotation); - } - } - - if ($annotations = $this->getAnnotation('return')) { - foreach ($annotations as $annotation) { - $docComment .= sprintf("@return %s\n", $annotation); - } - } - - $docComment .= "*/\n"; - - return $docComment; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - $annotations = $this->getAnnotations(); - return array_key_exists($name, $annotations); - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return string|array|null - */ - public function getAnnotation($name) - { - $annotations = $this->getAnnotations(); - if (array_key_exists($name, $annotations)) { - return $annotations[$name]; - } - return null; - } - - /** - * Retrieves a property or method value. - * - * @param string $name Property name - * @return mixed - */ - public function __get($name) - { - $key = ucfirst($name); - if (isset(self::$reflectionMethods[$this->reflectionType]['get' . $key])) { - return $this->{'get' . $key}(); - } - - if (isset(self::$reflectionMethods[$this->reflectionType]['is' . $key])) { - return $this->{'is' . $key}(); - } - - return null; - } - - /** - * Checks if the given property exists. - * - * @param mixed $name Property name - * @return boolean - */ - public function __isset($name) - { - $key = ucfirst($name); - return isset(self::$reflectionMethods[$this->reflectionType]['get' . $key]) || isset(self::$reflectionMethods[$this->reflectionType]['is' . $key]); - } -} diff --git a/apigen/ApiGen/ReflectionParameter.php b/apigen/ApiGen/ReflectionParameter.php deleted file mode 100644 index 9157d71b2f4..00000000000 --- a/apigen/ApiGen/ReflectionParameter.php +++ /dev/null @@ -1,215 +0,0 @@ -isArray()) { - return 'array'; - } elseif ($this->isCallable()) { - return 'callable'; - } elseif ($className = $this->getClassName()) { - return $className; - } elseif ($annotations = $this->getDeclaringFunction()->getAnnotation('param')) { - if (!empty($annotations[$this->getPosition()])) { - list($types) = preg_split('~\s+|$~', $annotations[$this->getPosition()], 2); - if (!empty($types) && '$' !== $types[0]) { - return $types; - } - } - } - - return 'mixed'; - } - - /** - * Returns the part of the source code defining the parameter default value. - * - * @return string - */ - public function getDefaultValueDefinition() - { - return $this->reflection->getDefaultValueDefinition(); - } - - /** - * Retutns if a default value for the parameter is available. - * - * @return boolean - */ - public function isDefaultValueAvailable() - { - return $this->reflection->isDefaultValueAvailable(); - } - - /** - * Returns the position within all parameters. - * - * @return integer - */ - public function getPosition() - { - return $this->reflection->position; - } - - /** - * Returns if the parameter expects an array. - * - * @return boolean - */ - public function isArray() - { - return $this->reflection->isArray(); - } - - /** - * Returns if the parameter expects a callback. - * - * @return boolean - */ - public function isCallable() - { - return $this->reflection->isCallable(); - } - - /** - * Returns reflection of the required class of the parameter. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getClass() - { - $className = $this->reflection->getClassName(); - return null === $className ? null : self::$parsedClasses[$className]; - } - - /** - * Returns the required class name of the value. - * - * @return string|null - */ - public function getClassName() - { - return $this->reflection->getClassName(); - } - - /** - * Returns if the the parameter allows NULL. - * - * @return boolean - */ - public function allowsNull() - { - return $this->reflection->allowsNull(); - } - - /** - * Returns if the parameter is optional. - * - * @return boolean - */ - public function isOptional() - { - return $this->reflection->isOptional(); - } - - /** - * Returns if the parameter value is passed by reference. - * - * @return boolean - */ - public function isPassedByReference() - { - return $this->reflection->isPassedByReference(); - } - - /** - * Returns if the paramter value can be passed by value. - * - * @return boolean - */ - public function canBePassedByValue() - { - return $this->reflection->canBePassedByValue(); - } - - /** - * Returns the declaring function. - * - * @return \ApiGen\ReflectionFunctionBase - */ - public function getDeclaringFunction() - { - $functionName = $this->reflection->getDeclaringFunctionName(); - - if ($className = $this->reflection->getDeclaringClassName()) { - return self::$parsedClasses[$className]->getMethod($functionName); - } else { - return self::$parsedFunctions[$functionName]; - } - } - - /** - * Returns the declaring function name. - * - * @return string - */ - public function getDeclaringFunctionName() - { - return $this->reflection->getDeclaringFunctionName(); - } - - /** - * Returns the function/method declaring class. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringClass() - { - $className = $this->reflection->getDeclaringClassName(); - return null === $className ? null : self::$parsedClasses[$className]; - } - - /** - * Returns the declaring class name. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->reflection->getDeclaringClassName(); - } - - /** - * If the parameter can be used unlimited. - * - * @return boolean - */ - public function isUnlimited() - { - return false; - } -} diff --git a/apigen/ApiGen/ReflectionParameterMagic.php b/apigen/ApiGen/ReflectionParameterMagic.php deleted file mode 100644 index 287696b1547..00000000000 --- a/apigen/ApiGen/ReflectionParameterMagic.php +++ /dev/null @@ -1,479 +0,0 @@ -reflectionType = get_class($this); - if (!isset(self::$reflectionMethods[$this->reflectionType])) { - self::$reflectionMethods[$this->reflectionType] = array_flip(get_class_methods($this)); - } - } - - /** - * Sets parameter name. - * - * @param string $name - * @return \ApiGen\ReflectionParameterMagic - */ - public function setName($name) - { - $this->name = (string) $name; - return $this; - } - - /** - * Sets type hint. - * - * @param string $typeHint - * @return \ApiGen\ReflectionParameterMagic - */ - public function setTypeHint($typeHint) - { - $this->typeHint = (string) $typeHint; - return $this; - } - - /** - * Sets position of the parameter in the function/method. - * - * @param integer $position - * @return \ApiGen\ReflectionParameterMagic - */ - public function setPosition($position) - { - $this->position = (int) $position; - return $this; - } - - /** - * Sets the part of the source code defining the parameter default value. - * - * @param string|null $defaultValueDefinition - * @return \ApiGen\ReflectionParameterMagic - */ - public function setDefaultValueDefinition($defaultValueDefinition) - { - $this->defaultValueDefinition = $defaultValueDefinition; - return $this; - } - - /** - * Sets if the parameter can be used unlimited times. - * - * @param boolean $unlimited - * @return \ApiGen\ReflectionParameterMagic - */ - public function setUnlimited($unlimited) - { - $this->unlimited = (bool) $unlimited; - return $this; - } - - /** - * Sets if the parameter value is passed by reference. - * - * @param boolean $passedByReference - * @return \ApiGen\ReflectionParameterMagic - */ - public function setPassedByReference($passedByReference) - { - $this->passedByReference = (bool) $passedByReference; - return $this; - } - - /** - * Sets declaring function. - * - * @param \ApiGen\ReflectionFunctionBase $declaringFunction - * @return \ApiGen\ReflectionParameterMagic - */ - public function setDeclaringFunction(ReflectionFunctionBase $declaringFunction) - { - $this->declaringFunction = $declaringFunction; - return $this; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->declaringFunction->getBroker(); - } - - /** - * Returns the name. - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the type hint. - * - * @return string - */ - public function getTypeHint() - { - return $this->typeHint; - } - - /** - * Returns the file name the parameter is defined in. - * - * @return string - */ - public function getFileName() - { - return $this->declaringFunction->getFileName(); - } - - /** - * Returns if the reflection object is internal. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns if the reflection object is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return true; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return true; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return str_replace('()', '($' . $this->name . ')', $this->declaringFunction->getPrettyName()); - } - - /** - * Returns the declaring class. - * - * @return \Apigen\ReflectionClass|null - */ - public function getDeclaringClass() - { - return $this->declaringFunction->getDeclaringClass(); - } - - /** - * Returns the declaring class name. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->declaringFunction->getDeclaringClassName(); - } - - /** - * Returns the declaring function. - * - * @return \ApiGen\ReflectionFunctionBase - */ - public function getDeclaringFunction() - { - return $this->declaringFunction; - } - - /** - * Returns the declaring function name. - * - * @return string - */ - public function getDeclaringFunctionName() - { - return $this->declaringFunction->getName(); - } - - /** - * Returns the definition start line number in the file. - * - * @return integer - */ - public function getStartLine() - { - return $this->declaringFunction->getStartLine(); - } - - /** - * Returns the definition end line number in the file. - * - * @return integer - */ - public function getEndLine() - { - return $this->declaringFunction->getEndLine(); - } - - /** - * Returns the appropriate docblock definition. - * - * @return string|boolean - */ - public function getDocComment() - { - return false; - } - - /** - * Returns the part of the source code defining the parameter default value. - * - * @return string - */ - public function getDefaultValueDefinition() - { - return $this->defaultValueDefinition; - } - - /** - * Returns if a default value for the parameter is available. - * - * @return boolean - */ - public function isDefaultValueAvailable() - { - return null !== $this->defaultValueDefinition; - } - - /** - * Returns the position within all parameters. - * - * @return integer - */ - public function getPosition() - { - return $this->position; - } - - /** - * Returns if the parameter expects an array. - * - * @return boolean - */ - public function isArray() - { - return TokenReflection\ReflectionParameter::ARRAY_TYPE_HINT === $this->typeHint; - } - - public function isCallable() - { - return TokenReflection\ReflectionParameter::CALLABLE_TYPE_HINT === $this->typeHint; - } - - /** - * Returns reflection of the required class of the value. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getClass() - { - $className = $this->getClassName(); - return null === $className ? null : self::$parsedClasses[$className]; - } - - /** - * Returns the required class name of the value. - * - * @return string|null - */ - public function getClassName() - { - if ($this->isArray() || $this->isCallable()) { - return null; - } - - if (isset(self::$parsedClasses[$this->typeHint])) { - return $typeHint; - } - - return null; - } - - /** - * Returns if the the parameter allows NULL. - * - * @return boolean - */ - public function allowsNull() - { - if ($this->isArray() || $this->isCallable()) { - return 'null' === strtolower($this->defaultValueDefinition); - } - - return !empty($this->defaultValueDefinition); - } - - /** - * Returns if the parameter is optional. - * - * @return boolean - */ - public function isOptional() - { - return $this->isDefaultValueAvailable(); - } - - /** - * Returns if the parameter value is passed by reference. - * - * @return boolean - */ - public function isPassedByReference() - { - return $this->passedByReference; - } - - /** - * Returns if the parameter value can be passed by value. - * - * @return boolean - */ - public function canBePassedByValue() - { - return false; - } - - /** - * Returns if the parameter can be used unlimited times. - * - * @return boolean - */ - public function isUnlimited() - { - return $this->unlimited; - } - - /** - * Retrieves a property or method value. - * - * @param string $name Property name - * @return mixed - */ - public function __get($name) - { - $key = ucfirst($name); - if (isset(self::$reflectionMethods[$this->reflectionType]['get' . $key])) { - return $this->{'get' . $key}(); - } - - if (isset(self::$reflectionMethods[$this->reflectionType]['is' . $key])) { - return $this->{'is' . $key}(); - } - - return null; - } - - /** - * Checks if the given property exists. - * - * @param mixed $name Property name - * @return boolean - */ - public function __isset($name) - { - $key = ucfirst($name); - return isset(self::$reflectionMethods[$this->reflectionType]['get' . $key]) || isset(self::$reflectionMethods[$this->reflectionType]['is' . $key]); - } -} diff --git a/apigen/ApiGen/ReflectionProperty.php b/apigen/ApiGen/ReflectionProperty.php deleted file mode 100644 index 414cb1c388b..00000000000 --- a/apigen/ApiGen/ReflectionProperty.php +++ /dev/null @@ -1,214 +0,0 @@ -getAnnotation('var')) { - list($types) = preg_split('~\s+|$~', $annotations[0], 2); - if (!empty($types) && '$' !== $types[0]) { - return $types; - } - } - - try { - $type = gettype($this->getDefaultValue()); - if ('null' !== strtolower($type)) { - return $type; - } - } catch (\Exception $e) { - // Nothing - } - - return 'mixed'; - } - - /** - * Returns the property declaring class. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringClass() - { - $className = $this->reflection->getDeclaringClassName(); - return null === $className ? null : self::$parsedClasses[$className]; - } - - /** - * Returns the name of the declaring class. - * - * @return string - */ - public function getDeclaringClassName() - { - return $this->reflection->getDeclaringClassName(); - } - - /** - * Returns the property default value. - * - * @return mixed - */ - public function getDefaultValue() - { - return $this->reflection->getDefaultValue(); - } - - /** - * Returns the part of the source code defining the property default value. - * - * @return string - */ - public function getDefaultValueDefinition() - { - return $this->reflection->getDefaultValueDefinition(); - } - - /** - * Returns if the property was created at compile time. - * - * @return boolean - */ - public function isDefault() - { - return $this->reflection->isDefault(); - } - - /** - * Returns property modifiers. - * - * @return integer - */ - public function getModifiers() - { - return $this->reflection->getModifiers(); - } - - /** - * Returns if the property is private. - * - * @return boolean - */ - public function isPrivate() - { - return $this->reflection->isPrivate(); - } - - /** - * Returns if the property is protected. - * - * @return boolean - */ - public function isProtected() - { - return $this->reflection->isProtected(); - } - - /** - * Returns if the property is public. - * - * @return boolean - */ - public function isPublic() - { - return $this->reflection->isPublic(); - } - - /** - * Returns if the poperty is static. - * - * @return boolean - */ - public function isStatic() - { - return $this->reflection->isStatic(); - } - - /** - * Returns the property declaring trait. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringTrait() - { - $traitName = $this->reflection->getDeclaringTraitName(); - return null === $traitName ? null : self::$parsedClasses[$traitName]; - } - - /** - * Returns the declaring trait name. - * - * @return string|null - */ - public function getDeclaringTraitName() - { - return $this->reflection->getDeclaringTraitName(); - } - - /** - * Returns if the property is valid. - * - * @return boolean - */ - public function isValid() - { - if ($class = $this->getDeclaringClass()) { - return $class->isValid(); - } - - return true; - } -} diff --git a/apigen/ApiGen/ReflectionPropertyMagic.php b/apigen/ApiGen/ReflectionPropertyMagic.php deleted file mode 100644 index 98e7e2477ef..00000000000 --- a/apigen/ApiGen/ReflectionPropertyMagic.php +++ /dev/null @@ -1,651 +0,0 @@ -reflectionType = get_class($this); - if (!isset(self::$reflectionMethods[$this->reflectionType])) { - self::$reflectionMethods[$this->reflectionType] = array_flip(get_class_methods($this)); - } - } - - /** - * Sets property name. - * - * @param string $name - * @return \Apigen\ReflectionPropertyMagic - */ - public function setName($name) - { - $this->name = (string) $name; - return $this; - - } - - /** - * Sets type hint. - * - * @param string $typeHint - * @return \ApiGen\ReflectionParameterUnlimited - */ - public function setTypeHint($typeHint) - { - $this->typeHint = (string) $typeHint; - return $this; - } - - /** - * Sets short description. - * - * @param string $shortDescription - * @return \Apigen\ReflectionPropertyMagic - */ - public function setShortDescription($shortDescription) - { - $this->shortDescription = (string) $shortDescription; - return $this; - } - - /** - * Sets start line. - * - * @param integer $startLine - * @return \Apigen\ReflectionPropertyMagic - */ - public function setStartLine($startLine) - { - $this->startLine = (int) $startLine; - return $this; - } - - /** - * Sets end line. - * - * @param integer $endLine - * @return \Apigen\ReflectionPropertyMagic - */ - public function setEndLine($endLine) - { - $this->endLine = (int) $endLine; - return $this; - } - - /** - * Sets if the property is read-only. - * - * @param boolean $readOnly - * @return \Apigen\ReflectionPropertyMagic - */ - public function setReadOnly($readOnly) - { - $this->readOnly = (bool) $readOnly; - return $this; - } - - /** - * Sets if the property is write only. - * - * @param boolean $writeOnly - * @return \Apigen\ReflectionPropertyMagic - */ - public function setWriteOnly($writeOnly) - { - $this->writeOnly = (bool) $writeOnly; - return $this; - } - - /** - * Sets declaring class. - * - * @param \ApiGen\ReflectionClass $declaringClass - * @return \ApiGen\ReflectionPropertyMagic - */ - public function setDeclaringClass(ReflectionClass $declaringClass) - { - $this->declaringClass = $declaringClass; - return $this; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->declaringClass->getBroker(); - } - - /** - * Returns the start position in the file token stream. - * - * @return integer - */ - public function getStartPosition() - { - return $this->declaringClass->getStartPosition(); - } - - /** - * Returns the end position in the file token stream. - * - * @return integer - */ - public function getEndPosition() - { - return $this->declaringClass->getEndPosition(); - } - - /** - * Returns the name. - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the type hint. - * - * @return string - */ - public function getTypeHint() - { - return $this->typeHint; - } - - /** - * Returns the short description. - * - * @return string - */ - public function getShortDescription() - { - return $this->shortDescription; - } - - /** - * Returns the long description. - * - * @return string - */ - public function getLongDescription() - { - return $this->shortDescription; - } - - /** - * Returns the definition start line number in the file. - * - * @return integer - */ - public function getStartLine() - { - return $this->startLine; - } - - /** - * Returns the definition end line number in the file. - * - * @return integer - */ - public function getEndLine() - { - return $this->endLine; - } - - /** - * Returns if the property is read-only. - * - * @return boolean - */ - public function isReadOnly() - { - return $this->readOnly; - } - - /** - * Returns if the property is write-only. - * - * @return boolean - */ - public function isWriteOnly() - { - return $this->writeOnly; - } - - /** - * Returns if the property is magic. - * - * @return boolean - */ - public function isMagic() - { - return true; - } - - /** - * Returns the PHP extension reflection. - * - * @return \ApiGen\ReflectionExtension|null - */ - public function getExtension() - { - return null; - } - - /** - * Returns the PHP extension name. - * - * @return boolean - */ - public function getExtensionName() - { - return false; - } - - /** - * Returns if the property should be documented. - * - * @return boolean - */ - public function isDocumented() - { - if (null === $this->isDocumented) { - $this->isDocumented = self::$config->deprecated || !$this->isDeprecated(); - } - - return $this->isDocumented; - } - - /** - * Returns if the property is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return $this->declaringClass->isDeprecated(); - } - - /** - * Returns property package name (including subpackage name). - * - * @return string - */ - public function getPackageName() - { - return $this->declaringClass->getPackageName(); - } - - /** - * Returns property namespace name. - * - * @return string - */ - public function getNamespaceName() - { - return $this->declaringClass->getNamespaceName(); - } - - /** - * Returns property annotations. - * - * @return array - */ - public function getAnnotations() - { - if (null === $this->annotations) { - $this->annotations = array(); - } - return $this->annotations; - } - - /** - * Returns the property declaring class. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringClass() - { - return $this->declaringClass; - } - - /** - * Returns the name of the declaring class. - * - * @return string - */ - public function getDeclaringClassName() - { - return $this->declaringClass->getName(); - } - - /** - * Returns the property default value. - * - * @return mixed - */ - public function getDefaultValue() - { - return null; - } - - /** - * Returns the part of the source code defining the property default value. - * - * @return string - */ - public function getDefaultValueDefinition() - { - return ''; - } - - /** - * Returns if the property was created at compile time. - * - * @return boolean - */ - public function isDefault() - { - return false; - } - - /** - * Returns property modifiers. - * - * @return integer - */ - public function getModifiers() - { - return InternalReflectionProperty::IS_PUBLIC; - } - - /** - * Returns if the property is private. - * - * @return boolean - */ - public function isPrivate() - { - return false; - } - - /** - * Returns if the property is protected. - * - * @return boolean - */ - public function isProtected() - { - return false; - } - - /** - * Returns if the property is public. - * - * @return boolean - */ - public function isPublic() - { - return true; - } - - /** - * Returns if the poperty is static. - * - * @return boolean - */ - public function isStatic() - { - return false; - } - - /** - * Returns if the property is internal. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns the property declaring trait. - * - * @return \ApiGen\ReflectionClass|null - */ - public function getDeclaringTrait() - { - return $this->declaringClass->isTrait() ? $this->declaringClass : null; - } - - /** - * Returns the declaring trait name. - * - * @return string|null - */ - public function getDeclaringTraitName() - { - if ($declaringTrait = $this->getDeclaringTrait()) { - return $declaringTrait->getName(); - } - return null; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->declaringClass->getNamespaceAliases(); - } - - /** - * Returns an property pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return sprintf('%s::$%s', $this->declaringClass->getName(), $this->name); - } - - /** - * Returns the file name the property is defined in. - * - * @return string - */ - public function getFileName() - { - return $this->declaringClass->getFileName(); - } - - /** - * Returns if the property is user defined. - - * @return boolean - */ - public function isUserDefined() - { - return true; - } - - /** - * Returns if the property comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return true; - } - - /** - * Returns the appropriate docblock definition. - * - * @return string|boolean - */ - public function getDocComment() - { - $docComment = "/**\n"; - - if (!empty($this->shortDescription)) { - $docComment .= $this->shortDescription . "\n\n"; - } - - if ($annotations = $this->getAnnotation('var')) { - $docComment .= sprintf("@var %s\n", $annotations[0]); - } - - $docComment .= "*/\n"; - - return $docComment; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - $annotations = $this->getAnnotations(); - return array_key_exists($name, $annotations); - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return string|array|null - */ - public function getAnnotation($name) - { - $annotations = $this->getAnnotations(); - if (array_key_exists($name, $annotations)) { - return $annotations[$name]; - } - return null; - } - - /** - * Retrieves a property or method value. - * - * @param string $name Property name - * @return mixed - */ - public function __get($name) - { - $key = ucfirst($name); - if (isset(self::$reflectionMethods[$this->reflectionType]['get' . $key])) { - return $this->{'get' . $key}(); - } - - if (isset(self::$reflectionMethods[$this->reflectionType]['is' . $key])) { - return $this->{'is' . $key}(); - } - - return null; - } - - /** - * Checks if the given property exists. - * - * @param mixed $name Property name - * @return boolean - */ - public function __isset($name) - { - $key = ucfirst($name); - return isset(self::$reflectionMethods[$this->reflectionType]['get' . $key]) || isset(self::$reflectionMethods[$this->reflectionType]['is' . $key]); - } -} diff --git a/apigen/ApiGen/SourceFilesFilterIterator.php b/apigen/ApiGen/SourceFilesFilterIterator.php deleted file mode 100644 index dc841088a8b..00000000000 --- a/apigen/ApiGen/SourceFilesFilterIterator.php +++ /dev/null @@ -1,75 +0,0 @@ -excludeMasks = $excludeMasks; - } - - /** - * Returns if the current file/directory should be processed. - * - * @return boolean - */ - public function accept() { - /** @var \SplFileInfo */ - $current = $this->current(); - - foreach ($this->excludeMasks as $mask) { - if (fnmatch($mask, $current->getPathName(), FNM_NOESCAPE)) { - return false; - } - } - - if (!is_readable($current->getPathname())) { - throw new \InvalidArgumentException(sprintf('File/directory "%s" is not readable.', $current->getPathname())); - } - - return true; - } - - /** - * Returns the iterator of the current element's children. - * - * @return \ApiGen\SourceFilesFilterIterator - */ - public function getChildren() - { - return new static($this->getInnerIterator()->getChildren(), $this->excludeMasks); - } -} diff --git a/apigen/ApiGen/Template.php b/apigen/ApiGen/Template.php deleted file mode 100644 index ea97892e66c..00000000000 --- a/apigen/ApiGen/Template.php +++ /dev/null @@ -1,809 +0,0 @@ -generator = $generator; - $this->config = $generator->getConfig(); - - $that = $this; - - // Output in HTML5 - Nette\Utils\Html::$xhtml = false; - - // FSHL - $fshl = new FSHL\Highlighter(new FSHL\Output\Html()); - $fshl->setLexer(new FSHL\Lexer\Php()); - - // Texy - $this->texy = new \Texy(); - $this->texy->allowedTags = array_flip($this->config->allowedHtml); - $this->texy->allowed['list/definition'] = false; - $this->texy->allowed['phrase/em-alt'] = false; - $this->texy->allowed['longwords'] = false; - $this->texy->allowed['typography'] = false; - $this->texy->linkModule->shorten = false; - // Highlighting ,
-		$this->texy->addHandler('beforeParse', function($texy, &$text, $singleLine) {
-			$text = preg_replace('~(.+?)~', '#code#\\1#/code#', $text);
-		});
-		$this->texy->registerLinePattern(
-			function($parser, $matches, $name) use ($fshl) {
-				return \TexyHtml::el('code', $fshl->highlight($matches[1]));
-			},
-			'~#code#(.+?)#/code#~',
-			'codeInlineSyntax'
-		);
-		$this->texy->registerBlockPattern(
-			function($parser, $matches, $name) use ($fshl) {
-				if ('code' === $matches[1]) {
-					$lines = array_filter(explode("\n", $matches[2]));
-					if (!empty($lines)) {
-						$firstLine = array_shift($lines);
-
-						$indent = '';
-						$li = 0;
-
-						while (isset($firstLine[$li]) && preg_match('~\s~', $firstLine[$li])) {
-							foreach ($lines as $line) {
-								if (!isset($line[$li]) || $firstLine[$li] !== $line[$li]) {
-									break 2;
-								}
-							}
-
-							$indent .= $firstLine[$li++];
-						}
-
-						if (!empty($indent)) {
-							$matches[2] = str_replace(
-								"\n" . $indent,
-								"\n",
-								0 === strpos($matches[2], $indent) ? substr($matches[2], $li) : $matches[2]
-							);
-						}
-					}
-
-					$content = $fshl->highlight($matches[2]);
-				} else {
-					$content = htmlspecialchars($matches[2]);
-				}
-
-				$content = $parser->getTexy()->protect($content, \Texy::CONTENT_BLOCK);
-				return \TexyHtml::el('pre', $content);
-			},
-			'~<(code|pre)>(.+?)~s',
-			'codeBlockSyntax'
-		);
-
-		// Common operations
-		$this->registerHelperLoader('Nette\Templating\Helpers::loader');
-
-		// PHP source highlight
-		$this->registerHelper('highlightPHP', function($source, $context) use ($that, $fshl) {
-			return $that->resolveLink($that->getTypeName($source), $context) ?: $fshl->highlight((string) $source);
-		});
-		$this->registerHelper('highlightValue', function($definition, $context) use ($that) {
-			return $that->highlightPHP(preg_replace('~^(?:[ ]{4}|\t)~m', '', $definition), $context);
-		});
-
-		// Urls
-		$this->registerHelper('packageUrl', new Nette\Callback($this, 'getPackageUrl'));
-		$this->registerHelper('namespaceUrl', new Nette\Callback($this, 'getNamespaceUrl'));
-		$this->registerHelper('groupUrl', new Nette\Callback($this, 'getGroupUrl'));
-		$this->registerHelper('classUrl', new Nette\Callback($this, 'getClassUrl'));
-		$this->registerHelper('methodUrl', new Nette\Callback($this, 'getMethodUrl'));
-		$this->registerHelper('propertyUrl', new Nette\Callback($this, 'getPropertyUrl'));
-		$this->registerHelper('constantUrl', new Nette\Callback($this, 'getConstantUrl'));
-		$this->registerHelper('functionUrl', new Nette\Callback($this, 'getFunctionUrl'));
-		$this->registerHelper('elementUrl', new Nette\Callback($this, 'getElementUrl'));
-		$this->registerHelper('sourceUrl', new Nette\Callback($this, 'getSourceUrl'));
-		$this->registerHelper('manualUrl', new Nette\Callback($this, 'getManualUrl'));
-
-		// Packages & namespaces
-		$this->registerHelper('packageLinks', new Nette\Callback($this, 'getPackageLinks'));
-		$this->registerHelper('namespaceLinks', new Nette\Callback($this, 'getNamespaceLinks'));
-		$this->registerHelper('subgroupName', function($groupName) {
-			if ($pos = strrpos($groupName, '\\')) {
-				return substr($groupName, $pos + 1);
-			}
-			return $groupName;
-		});
-
-		// Types
-		$this->registerHelper('typeLinks', new Nette\Callback($this, 'getTypeLinks'));
-
-		// Docblock descriptions
-		$this->registerHelper('description', function($annotation, $context) use ($that) {
-			$description = trim(strpbrk($annotation, "\n\r\t $"));
-
-			if ($context instanceof ReflectionParameter) {
-				$description = preg_replace('~^(\\$' . $context->getName() . '(?:,\\.{3})?)(\s+|$)~i', '\\2', $description, 1);
-				$context = $context->getDeclaringFunction();
-			}
-			return $that->doc($description, $context);
-		});
-		$this->registerHelper('shortDescription', function($element, $block = false) use ($that) {
-			return $that->doc($element->getShortDescription(), $element, $block);
-		});
-		$this->registerHelper('longDescription', function($element) use ($that) {
-			$long = $element->getLongDescription();
-
-			// Merge lines
-			$long = preg_replace_callback('~(?:<(code|pre)>.+?)|([^<]*)~s', function($matches) {
-				return !empty($matches[2])
-					? preg_replace('~\n(?:\t|[ ])+~', ' ', $matches[2])
-					: $matches[0];
-			}, $long);
-
-			return $that->doc($long, $element, true);
-		});
-
-		// Individual annotations processing
-		$this->registerHelper('annotation', function($value, $name, $context) use ($that, $generator) {
-			switch ($name) {
-				case 'param':
-				case 'return':
-				case 'throws':
-					$description = $that->description($value, $context);
-					return sprintf('%s%s', $that->getTypeLinks($value, $context), $description ? '
' . $description : ''); - case 'license': - list($url, $description) = $that->split($value); - return $that->link($url, $description ?: $url); - case 'link': - list($url, $description) = $that->split($value); - if (Nette\Utils\Validators::isUrl($url)) { - return $that->link($url, $description ?: $url); - } - break; - case 'see': - $doc = array(); - foreach (preg_split('~\\s*,\\s*~', $value) as $link) { - if (null !== $generator->resolveElement($link, $context)) { - $doc[] = sprintf('%s', $that->getTypeLinks($link, $context)); - } else { - $doc[] = $that->doc($link, $context); - } - } - return implode(', ', $doc); - case 'uses': - case 'usedby': - list($link, $description) = $that->split($value); - $separator = $context instanceof ReflectionClass || !$description ? ' ' : '
'; - if (null !== $generator->resolveElement($link, $context)) { - return sprintf('%s%s%s', $that->getTypeLinks($link, $context), $separator, $description); - } - break; - default: - break; - } - - // Default - return $that->doc($value, $context); - }); - - $todo = $this->config->todo; - $internal = $this->config->internal; - $this->registerHelper('annotationFilter', function(array $annotations, array $filter = array()) use ($todo, $internal) { - // Filtered, unsupported or deprecated annotations - static $filtered = array( - 'package', 'subpackage', 'property', 'property-read', 'property-write', 'method', 'abstract', - 'access', 'final', 'filesource', 'global', 'name', 'static', 'staticvar' - ); - foreach ($filtered as $annotation) { - unset($annotations[$annotation]); - } - - // Custom filter - foreach ($filter as $annotation) { - unset($annotations[$annotation]); - } - - // Show/hide internal - if (!$internal) { - unset($annotations['internal']); - } - - // Show/hide tasks - if (!$todo) { - unset($annotations['todo']); - } - - return $annotations; - }); - - $this->registerHelper('annotationSort', function(array $annotations) { - uksort($annotations, function($one, $two) { - static $order = array( - 'deprecated' => 0, 'category' => 1, 'copyright' => 2, 'license' => 3, 'author' => 4, 'version' => 5, - 'since' => 6, 'see' => 7, 'uses' => 8, 'usedby' => 9, 'link' => 10, 'internal' => 11, - 'example' => 12, 'tutorial' => 13, 'todo' => 14 - ); - - if (isset($order[$one], $order[$two])) { - return $order[$one] - $order[$two]; - } elseif (isset($order[$one])) { - return -1; - } elseif (isset($order[$two])) { - return 1; - } else { - return strcasecmp($one, $two); - } - }); - return $annotations; - }); - - $this->registerHelper('annotationBeautify', function($annotation) { - static $names = array( - 'usedby' => 'Used by' - ); - - if (isset($names[$annotation])) { - return $names[$annotation]; - } - - return Nette\Utils\Strings::firstUpper($annotation); - }); - - // Static files versioning - $destination = $this->config->destination; - $this->registerHelper('staticFile', function($name) use ($destination) { - static $versions = array(); - - $filename = $destination . DIRECTORY_SEPARATOR . $name; - if (!isset($versions[$filename]) && is_file($filename)) { - $versions[$filename] = sprintf('%u', crc32(file_get_contents($filename))); - } - if (isset($versions[$filename])) { - $name .= '?' . $versions[$filename]; - } - return $name; - }); - - // Source anchors - $this->registerHelper('sourceAnchors', function($source) { - // Classes, interfaces, traits and exceptions - $source = preg_replace_callback('~((?:class|interface|trait)\\s+)(\\w+)~i', function($matches) { - $link = sprintf('%1$s', $matches[2]); - return $matches[1] . $link; - }, $source); - - // Methods and functions - $source = preg_replace_callback('~(function\\s+)(\\w+)~i', function($matches) { - $link = sprintf('%1$s', $matches[2]); - return $matches[1] . $link; - }, $source); - - // Constants - $source = preg_replace_callback('~(const)(.*?)(;)~is', function($matches) { - $links = preg_replace_callback('~(\\s|,)([A-Z_]+)(\\s+=)~', function($matches) { - return $matches[1] . sprintf('%1$s', $matches[2]) . $matches[3]; - }, $matches[2]); - return $matches[1] . $links . $matches[3]; - }, $source); - - // Properties - $source = preg_replace_callback('~((?:private|protected|public|var|static)\\s+)(.*?)(;)~is', function($matches) { - $links = preg_replace_callback('~()(\\$\\w+)~i', function($matches) { - return $matches[1] . sprintf('%1$s', $matches[2]); - }, $matches[2]); - return $matches[1] . $links . $matches[3]; - }, $source); - - return $source; - }); - - $this->registerHelper('urlize', array($this, 'urlize')); - - $this->registerHelper('relativePath', array($generator, 'getRelativePath')); - $this->registerHelper('resolveElement', array($generator, 'resolveElement')); - $this->registerHelper('getClass', array($generator, 'getClass')); - } - - /** - * Returns unified type value definition (class name or member data type). - * - * @param string $name - * @param boolean $trimNamespaceSeparator - * @return string - */ - public function getTypeName($name, $trimNamespaceSeparator = true) - { - static $names = array( - 'int' => 'integer', - 'bool' => 'boolean', - 'double' => 'float', - 'void' => '', - 'FALSE' => 'false', - 'TRUE' => 'true', - 'NULL' => 'null', - 'callback' => 'callable' - ); - - // Simple type - if (isset($names[$name])) { - return $names[$name]; - } - - // Class, constant or function - return $trimNamespaceSeparator ? ltrim($name, '\\') : $name; - } - - /** - * Returns links for types. - * - * @param string $annotation - * @param \ApiGen\ReflectionElement $context - * @return string - */ - public function getTypeLinks($annotation, ReflectionElement $context) - { - $links = array(); - - list($types) = $this->split($annotation); - if (!empty($types) && '$' === $types{0}) { - $types = null; - } - - if (empty($types)) { - $types = 'mixed'; - } - - foreach (explode('|', $types) as $type) { - $type = $this->getTypeName($type, false); - $links[] = $this->resolveLink($type, $context) ?: $this->escapeHtml(ltrim($type, '\\')); - } - - return implode('|', $links); - } - - /** - * Returns links for package/namespace and its parent packages. - * - * @param string $package - * @param boolean $last - * @return string - */ - public function getPackageLinks($package, $last = true) - { - if (empty($this->packages)) { - return $package; - } - - $links = array(); - - $parent = ''; - foreach (explode('\\', $package) as $part) { - $parent = ltrim($parent . '\\' . $part, '\\'); - $links[] = $last || $parent !== $package - ? $this->link($this->getPackageUrl($parent), $part) - : $this->escapeHtml($part); - } - - return implode('\\', $links); - } - - /** - * Returns links for namespace and its parent namespaces. - * - * @param string $namespace - * @param boolean $last - * @return string - */ - public function getNamespaceLinks($namespace, $last = true) - { - if (empty($this->namespaces)) { - return $namespace; - } - - $links = array(); - - $parent = ''; - foreach (explode('\\', $namespace) as $part) { - $parent = ltrim($parent . '\\' . $part, '\\'); - $links[] = $last || $parent !== $namespace - ? $this->link($this->getNamespaceUrl($parent), $part) - : $this->escapeHtml($part); - } - - return implode('\\', $links); - } - - /** - * Returns a link to a namespace summary file. - * - * @param string $namespaceName Namespace name - * @return string - */ - public function getNamespaceUrl($namespaceName) - { - return sprintf($this->config->template['templates']['main']['namespace']['filename'], $this->urlize($namespaceName)); - } - - /** - * Returns a link to a package summary file. - * - * @param string $packageName Package name - * @return string - */ - public function getPackageUrl($packageName) - { - return sprintf($this->config->template['templates']['main']['package']['filename'], $this->urlize($packageName)); - } - - /** - * Returns a link to a group summary file. - * - * @param string $groupName Group name - * @return string - */ - public function getGroupUrl($groupName) - { - if (!empty($this->packages)) { - return $this->getPackageUrl($groupName); - } - - return $this->getNamespaceUrl($groupName); - } - - /** - * Returns a link to class summary file. - * - * @param string|\ApiGen\ReflectionClass $class Class reflection or name - * @return string - */ - public function getClassUrl($class) - { - $className = $class instanceof ReflectionClass ? $class->getName() : $class; - return sprintf($this->config->template['templates']['main']['class']['filename'], $this->urlize($className)); - } - - /** - * Returns a link to method in class summary file. - * - * @param \ApiGen\ReflectionMethod $method Method reflection - * @param \ApiGen\ReflectionClass $class Method declaring class - * @return string - */ - public function getMethodUrl(ReflectionMethod $method, ReflectionClass $class = null) - { - $className = null !== $class ? $class->getName() : $method->getDeclaringClassName(); - return $this->getClassUrl($className) . '#' . ($method->isMagic() ? 'm' : '') . '_' . ($method->getOriginalName() ?: $method->getName()); - } - - /** - * Returns a link to property in class summary file. - * - * @param \ApiGen\ReflectionProperty $property Property reflection - * @param \ApiGen\ReflectionClass $class Property declaring class - * @return string - */ - public function getPropertyUrl(ReflectionProperty $property, ReflectionClass $class = null) - { - $className = null !== $class ? $class->getName() : $property->getDeclaringClassName(); - return $this->getClassUrl($className) . '#' . ($property->isMagic() ? 'm' : '') . '$' . $property->getName(); - } - - /** - * Returns a link to constant in class summary file or to constant summary file. - * - * @param \ApiGen\ReflectionConstant $constant Constant reflection - * @return string - */ - public function getConstantUrl(ReflectionConstant $constant) - { - // Class constant - if ($className = $constant->getDeclaringClassName()) { - return $this->getClassUrl($className) . '#' . $constant->getName(); - } - // Constant in namespace or global space - return sprintf($this->config->template['templates']['main']['constant']['filename'], $this->urlize($constant->getName())); - } - - /** - * Returns a link to function summary file. - * - * @param \ApiGen\ReflectionFunction $function Function reflection - * @return string - */ - public function getFunctionUrl(ReflectionFunction $function) - { - return sprintf($this->config->template['templates']['main']['function']['filename'], $this->urlize($function->getName())); - } - - /** - * Returns a link to element summary file. - * - * @param \ApiGen\ReflectionElement $element Element reflection - * @return string - */ - public function getElementUrl(ReflectionElement $element) - { - if ($element instanceof ReflectionClass) { - return $this->getClassUrl($element); - } elseif ($element instanceof ReflectionMethod) { - return $this->getMethodUrl($element); - } elseif ($element instanceof ReflectionProperty) { - return $this->getPropertyUrl($element); - } elseif ($element instanceof ReflectionConstant) { - return $this->getConstantUrl($element); - } elseif ($element instanceof ReflectionFunction) { - return $this->getFunctionUrl($element); - } - } - - /** - * Returns a link to a element source code. - * - * @param \ApiGen\ReflectionElement $element Element reflection - * @param boolean $withLine Include file line number into the link - * @return string - */ - public function getSourceUrl(ReflectionElement $element, $withLine = true) - { - if ($element instanceof ReflectionClass || $element instanceof ReflectionFunction || ($element instanceof ReflectionConstant && null === $element->getDeclaringClassName())) { - $elementName = $element->getName(); - - if ($element instanceof ReflectionClass) { - $file = 'class-'; - } elseif ($element instanceof ReflectionConstant) { - $file = 'constant-'; - } elseif ($element instanceof ReflectionFunction) { - $file = 'function-'; - } - } else { - $elementName = $element->getDeclaringClassName(); - $file = 'class-'; - } - - $file .= $this->urlize($elementName); - - $lines = null; - if ($withLine) { - $lines = $element->getStartLine() !== $element->getEndLine() ? sprintf('%s-%s', $element->getStartLine(), $element->getEndLine()) : $element->getStartLine(); - } - - return sprintf($this->config->template['templates']['main']['source']['filename'], $file) . (null !== $lines ? '#' . $lines : ''); - } - - /** - * Returns a link to a element documentation at php.net. - * - * @param \ApiGen\ReflectionBase $element Element reflection - * @return string - */ - public function getManualUrl(ReflectionBase $element) - { - static $manual = 'http://php.net/manual'; - static $reservedClasses = array('stdClass', 'Closure', 'Directory'); - - // Extension - if ($element instanceof ReflectionExtension) { - $extensionName = strtolower($element->getName()); - if ('core' === $extensionName) { - return $manual; - } - - if ('date' === $extensionName) { - $extensionName = 'datetime'; - } - - return sprintf('%s/book.%s.php', $manual, $extensionName); - } - - // Class and its members - $class = $element instanceof ReflectionClass ? $element : $element->getDeclaringClass(); - - if (in_array($class->getName(), $reservedClasses)) { - return $manual . '/reserved.classes.php'; - } - - $className = strtolower($class->getName()); - $classUrl = sprintf('%s/class.%s.php', $manual, $className); - $elementName = strtolower(strtr(ltrim($element->getName(), '_'), '_', '-')); - - if ($element instanceof ReflectionClass) { - return $classUrl; - } elseif ($element instanceof ReflectionMethod) { - return sprintf('%s/%s.%s.php', $manual, $className, $elementName); - } elseif ($element instanceof ReflectionProperty) { - return sprintf('%s#%s.props.%s', $classUrl, $className, $elementName); - } elseif ($element instanceof ReflectionConstant) { - return sprintf('%s#%s.constants.%s', $classUrl, $className, $elementName); - } - } - - /** - * Tries to parse a definition of a class/method/property/constant/function and returns the appropriate link if successful. - * - * @param string $definition Definition - * @param \ApiGen\ReflectionElement $context Link context - * @return string|null - */ - public function resolveLink($definition, ReflectionElement $context) - { - if (empty($definition)) { - return null; - } - - $suffix = ''; - if ('[]' === substr($definition, -2)) { - $definition = substr($definition, 0, -2); - $suffix = '[]'; - } - - $element = $this->generator->resolveElement($definition, $context, $expectedName); - if (null === $element) { - return $expectedName; - } - - $classes = array(); - if ($element->isDeprecated()) { - $classes[] = 'deprecated'; - } - if (!$element->isValid()) { - $classes[] = 'invalid'; - } - - if ($element instanceof ReflectionClass) { - $link = $this->link($this->getClassUrl($element), $element->getName(), true, $classes); - } elseif ($element instanceof ReflectionConstant && null === $element->getDeclaringClassName()) { - $text = $element->inNamespace() - ? $this->escapeHtml($element->getNamespaceName()) . '\\' . $this->escapeHtml($element->getShortName()) . '' - : '' . $this->escapeHtml($element->getName()) . ''; - $link = $this->link($this->getConstantUrl($element), $text, false, $classes); - } elseif ($element instanceof ReflectionFunction) { - $link = $this->link($this->getFunctionUrl($element), $element->getName() . '()', true, $classes); - } else { - $text = $this->escapeHtml($element->getDeclaringClassName()); - if ($element instanceof ReflectionProperty) { - $url = $this->propertyUrl($element); - $text .= '::$' . $this->escapeHtml($element->getName()) . ''; - } elseif ($element instanceof ReflectionMethod) { - $url = $this->methodUrl($element); - $text .= '::' . $this->escapeHtml($element->getName()) . '()'; - } elseif ($element instanceof ReflectionConstant) { - $url = $this->constantUrl($element); - $text .= '::' . $this->escapeHtml($element->getName()) . ''; - } - - $link = $this->link($url, $text, false, $classes); - } - - return sprintf('%s', $link . $suffix); - } - - /** - * Resolves links in documentation. - * - * @param string $text Processed documentation text - * @param \ApiGen\ReflectionElement $context Reflection object - * @return string - */ - private function resolveLinks($text, ReflectionElement $context) - { - $that = $this; - return preg_replace_callback('~{@(?:link|see)\\s+([^}]+)}~', function ($matches) use ($context, $that) { - // Texy already added so it has to be stripped - list($url, $description) = $that->split(strip_tags($matches[1])); - if (Nette\Utils\Validators::isUrl($url)) { - return $that->link($url, $description ?: $url); - } - return $that->resolveLink($matches[1], $context) ?: $matches[1]; - }, $text); - } - - /** - * Resolves internal annotation. - * - * @param string $text - * @return string - */ - private function resolveInternal($text) - { - $internal = $this->config->internal; - return preg_replace_callback('~\\{@(\\w+)(?:(?:\\s+((?>(?R)|[^{}]+)*)\\})|\\})~', function($matches) use ($internal) { - // Replace only internal - if ('internal' !== $matches[1]) { - return $matches[0]; - } - return $internal && isset($matches[2]) ? $matches[2] : ''; - }, $text); - } - - /** - * Formats text as documentation block or line. - * - * @param string $text Text - * @param \ApiGen\ReflectionElement $context Reflection object - * @param boolean $block Parse text as block - * @return string - */ - public function doc($text, ReflectionElement $context, $block = false) - { - return $this->resolveLinks($this->texy->process($this->resolveInternal($text), !$block), $context); - } - - /** - * Parses annotation value. - * - * @param string $value - * @return array - */ - public function split($value) - { - return preg_split('~\s+|$~', $value, 2); - } - - /** - * Returns link. - * - * @param string $url - * @param string $text - * @param boolean $escape If the text should be escaped - * @param array $classes List of classes - * @return string - */ - public function link($url, $text, $escape = true, array $classes = array()) - { - $class = !empty($classes) ? sprintf(' class="%s"', implode(' ', $classes)) : ''; - return sprintf('%s', $url, $class, $escape ? $this->escapeHtml($text) : $text); - } - - /** - * Converts string to url safe characters. - * - * @param string $string - * @return string - */ - public function urlize($string) - { - return preg_replace('~[^\w]~', '.', $string); - } -} diff --git a/apigen/ApiGen/Tree.php b/apigen/ApiGen/Tree.php deleted file mode 100644 index 01f47b6b76b..00000000000 --- a/apigen/ApiGen/Tree.php +++ /dev/null @@ -1,90 +0,0 @@ -setPrefixPart(RecursiveTreeIterator::PREFIX_END_HAS_NEXT, self::HAS_NEXT); - $this->setPrefixPart(RecursiveTreeIterator::PREFIX_END_LAST, self::LAST); - $this->rewind(); - - $this->reflections = $reflections; - } - - /** - * Returns if the current item has a sibling on the same level. - * - * @return boolean - */ - public function hasSibling() - { - $prefix = $this->getPrefix(); - return !empty($prefix) && self::HAS_NEXT === substr($prefix, -1); - } - - /** - * Returns the current reflection. - * - * @return \ApiGen\Reflection - * @throws \UnexpectedValueException If current is not reflection array. - */ - public function current() - { - $className = $this->key(); - if (!isset($this->reflections[$className])) { - throw new RuntimeException(sprintf('Class "%s" is not in the reflection array', $className)); - } - - return $this->reflections[$className]; - } -} diff --git a/apigen/CHANGELOG.md b/apigen/CHANGELOG.md deleted file mode 100644 index a836e552830..00000000000 --- a/apigen/CHANGELOG.md +++ /dev/null @@ -1,128 +0,0 @@ -## ApiGen 2.8.0 (2012-09-08) ## - -* Added support for @property and @method annotations -* Added support for variable length parameters -* Enabled selection of more rows in source code -* Templates can specify minimum and maximum required ApiGen version -* Added template for 404 page -* Improved support for malformed @param annotations -* Fixed excluding files and directories and detecting non accessible files and directories -* Fixed internal error when no timezone is specified in php.ini -* Fixed autocomplate in Opera browser -* Nette framework updated to version 2.0.5 -* TokenReflection library updated to version 1.3.1 -* FSHL library updated to version 2.1.0 - -## ApiGen 2.7.0 (2012-07-15) ## - -* Support of custom template macros and helpers -* Information about overridden methods in class method list -* Template UX fixes -* Fixed bugs causing ApiGen to crash -* TokenReflection library updated to version 1.3.0 -* Bootstrap2 based template -* Removed template with frames - -## ApiGen 2.6.1 (2012-03-27) ## - -* Fixed resolving element names in annotations -* Nette framework updated to version 2.0.1 -* TokenReflection library updated to version 1.2.2 - -## ApiGen 2.6.0 (2012-03-11) ## - -* Better error reporting, especially about duplicate classes, functions and constants -* Character set autodetection is on by default -* Changed visualization of deprecated elements -* Improved packages parsing and visualization -* Improved @license and @link visualization -* Improved `````` parsing -* Added option ```--extensions``` to specify file extensions of parsed files -* Minor visualization improvements -* Fixed autocomplete for classes in namespaces -* TokenReflection library updated to version 1.2.0 - -## ApiGen 2.5.0 (2012-02-12) ## - -* Added option ```--groups``` for grouping classes, interfaces, traits and exceptions in the menu -* Added option ```--autocomplete``` for choosing elements in the search autocomplete -* Inheriting some annotations from the file-level docblock -* @uses annotations create a @usedby annotation in the target documentation -* Added warning for unknown options -* Added support of comma-separated values for @see -* Changed all path options to be relative to the configuration file -* Fixed dependencies check -* Nette framework updated to 2.0.0 stable version -* TokenReflection library updated to version 1.1.0 - -## ApiGen 2.4.1 (2012-01-25) ## - -* TokenReflection library updated to version 1.0.2 -* Nette framework updated to version 2.0.0RC1 - -## ApiGen 2.4.0 (2011-12-24) ## - -* TokenReflection library updated to version 1.0.0 -* Fixed support for older PHP versions of the 5.3 branch -* Option ```templateConfig``` is relative to the config file (was relative to cwd) - -## ApiGen 2.3.0 (2011-11-13) ## - -* Added support for default configuration file -* Added link to download documentation as ZIP archive -* Added option ```--charset``` and autodetection of charsets -* Added support for @ignore annotation -* Added PHAR support -* Added support for ClassName[] -* Added memory usage reporting in progressbar -* Improved templates for small screens -* Changed option name ```--undocumented``` to ```--report``` -* FSHL library updated to version 2.0.1 - -## ApiGen 2.2.1 (2011-10-26) ## - -* Fixed processing of magic constants -* Fixed resize.png -* TokenReflection library updated to version 1.0.0RC2 - -## ApiGen 2.2.0 (2011-10-16) ## - -* Added an option to check for updates -* Added an option to initially display elements in alphabetical order -* Added an option to generate the robots.txt file -* Added required extensions check -* Changed reporting of undocumented elements to the checkstyle format -* Improved deprecated elements highlighting -* Highlighting the linked source code line -* Unknown annotations are sorted alphabetically -* Fixed class parameter description parsing -* Fixed command line options parsing -* Fixed include path setting of the GitHub version -* Fixed frames template - -## ApiGen 2.1.0 (2011-09-04) ## - -* Experimental support of PHP 5.4 traits -* Added option ```--colors``` -* Added template with frames -* Added templates option to make element details expanded by default - -## ApiGen 2.0.3 (2011-08-22) ## - -* @param, @return and @throw annotations are inherited - -## ApiGen 2.0.2 (2011-07-21) ## - -* Fixed inherited methods listing -* Interfaces are not labeled "Abstract interface" -* Fixed Google CSE ID validation -* Fixed filtering by ```--exclude``` and ```--skip-doc-path``` -* Fixed exception output when using ```--debug``` - -## ApiGen 2.0.1 (2011-07-17) ## - -* Updated TokenReflection library to 1.0.0beta5 -* Requires FSHL 2.0.0RC -* Fixed url in footer - -## ApiGen 2.0.0 (2011-06-28) ## diff --git a/apigen/LICENSE.md b/apigen/LICENSE.md deleted file mode 100644 index afe8631e4e6..00000000000 --- a/apigen/LICENSE.md +++ /dev/null @@ -1,32 +0,0 @@ -# Licenses # - -You may use ApiGen under the terms of either the New BSD License or the GNU General Public License (GPL) version 2 or 3. - -The BSD License is recommended for most projects. It is easy to understand and it places almost no restrictions on what you can do with the framework. If the GPL fits better to your project, you can use the framework under this license. - -You don't have to notify anyone which license you are using. You can freely use ApiGen in commercial projects as long as the copyright header remains intact. - -## New BSD License ## - -Copyright (c) 2010 [David Grudl](http://davidgrudl.com) -Copyright (c) 2011-2012 [Jaroslav Hanslík](https://github.com/kukulich) -Copyright (c) 2011-2012 [Ondřej Nešpor](https://github.com/Andrewsville) - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -* Neither the name of "ApiGen" nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -## GNU General Public License ## - -GPL licenses are very very long, so instead of including them here we offer you URLs with full text: - -* [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) -* [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) diff --git a/apigen/README.md b/apigen/README.md deleted file mode 100644 index 14634f8959b..00000000000 --- a/apigen/README.md +++ /dev/null @@ -1,285 +0,0 @@ -# Welcome to ApiGen # - -ApiGen is the tool for creating professional API documentation from PHP source code, similar to discontinued phpDocumentor/phpDoc. - -ApiGen has support for PHP 5.3 namespaces, packages, linking between documentation, cross referencing to PHP standard classes and general documentation, creation of highlighted source code and experimental support for PHP 5.4 **traits**. - -## Support & Bug Reports ## - -For all support questions please use our [mailing list](https://groups.google.com/group/apigen). For bug reports and issues the [issue tracker](https://github.com/apigen/apigen/issues) is available. Changes between versions are described in the [change log](https://github.com/apigen/apigen/blob/master/CHANGELOG.md). - -## Features ## - -* Our own [TokenReflection library](https://github.com/Andrewsville/PHP-Token-Reflection) is used to describe the source code. It is **safe** (documented source code does not get included and thus parsed) and **simple** (you do not need to include or autoload all libraries you use in your source code). -* Detailed documentation of classes, functions and constants. -* Highlighted source code. -* Support of namespaces and packages with subpackages. -* Experimental support of traits. -* A page with trees of classes, interfaces, traits and exceptions. -* A page with a list of deprecated elements. -* A page with Todo tasks. -* Link to download documentation as ZIP archive. -* Checkstyle report of poorly documented elements. -* Support for docblock templates. -* Support for @inheritdoc. -* Support for {@link}. -* Active links in @see and @uses tags. -* Documentation of used internal PHP classes. -* Links to the start line in the highlighted source code for every described element. -* List of direct and indirect known subclasses, implementers and users for every class/interface/trait/exception. -* Check for a new version. -* Google CSE support with suggest. -* Google Analytics support. -* Support for multiple/custom templates. -* Sitemap and opensearch support. -* Support for different charsets and line endings. -* Lots of configuration options (see below). - -## Installation ## - -The preferred installation way is using the PEAR package but there are three more ways how to install ApiGen. - - -### PEAR ### - -PEAR is a distribution system for PHP packages. It is bundled with PHP since the 4.3 version and it is easy to use. - -The PEAR package contains only ApiGen itself. Its dependencies (Nette, Texy, FSHL and TokenReflection) have to be installed separately. But do not panic, the PEAR installer can take care of it. - -The easiest way is to use the PEAR auto discovery feature. In that case all you have to do is to type two commands. - -``` - pear config-set auto_discover 1 - pear install pear.apigen.org/apigen -``` - -If you don't want to use the auto discovery, you have to add PEAR channels of all ApiGen libraries manually. In this case you can install ApiGen by typing these commands. - -``` - pear channel-discover pear.apigen.org - pear channel-discover pear.nette.org - pear channel-discover pear.texy.info - pear channel-discover pear.kukulich.cz - pear channel-discover pear.andrewsville.cz - - pear install apigen/ApiGen -``` - -If you encounter a message like `WARNING: channel "pear.apigen.org" has updated its protocols, use "pear channel-update pear.apigen.org" to update`, you need to tell PEAR to update its information about the ApiGen channel using the suggested command. - -``` -pear channel-update pear.apigen.org -``` - -### Standalone package ### - -Using the standalone package is even easier than using the PEAR installer but it does not handle updates automatically. - -To download the actual release visit the [Downloads section](https://github.com/apigen/apigen/downloads). There you find separate packages for each release in two formats - zip and tar.gz. These packages are prepared by the ApiGen team and are truly standalone; they contain all required libraries in appropriate versions. You just need to extract the contents of an archive and you can start using ApiGen. - -### GitHub built archive ### - -GitHub allows you to download any repository as a zip or tar.gz archive. You can use this feature to download an archive with the current version of ApiGen. However this approach has one disadvantage. Such archive (in contrast to the standalone packages) does not contain required libraries. They are included as git submodules in the repository and GitHub simply ignores them when generating the archive. It means that you will have to obtain required libraries manually. - -### Cloning the repository ### - -The last way how to install ApiGen is simply to clone our repository. If you do so, remember to fetch and rebase to get new versions and do not forget to update submodules in the libs directory. - -## Usage ## - -``` - apigen --config [options] - apigen --source --destination [options] -``` - -As you can see, you can use ApiGen either by providing individual parameters via the command line or using a config file. Moreover you can combine the two methods and the command line parameters will have precedence over those in the config file. - -Every configuration option has to be followed by its value. And it is exactly the same to write ```--config=file.conf``` and ```--config file.conf```. The only exceptions are boolean options (those with yes|no values). When using these options on the command line you do not have to provide the "yes" value explicitly. If ommited, it is assumed that you wanted to turn the option on. So using ```--debug=yes``` and ```--debug``` does exactly the same (and the opposite is ```--debug=no```). - -Some options can have multiple values. To do so, you can either use them multiple times or separate their values by a comma. It means that ```--source=file1.php --source=file2.php``` and ```--source=file1.php,file2.php``` is exactly the same. - -### Options ### - -```--config|-c ``` - -Path to the config file. - -```--source|-s ``` **required** - -Path to the directory or file to be processed. You can use the parameter multiple times to provide a list of directories or files. All types of PHAR archives are supported (requires the PHAR extension). To process gz/bz2 compressed archives you need the appropriate extension (see requirements). - -```--destination|-d ``` **required** - -Documentation will be generated into this directory. - -```--extensions ``` - -List of allowed file extensions, default is "php". - -```--exclude ``` - -Directories and files matching this file mask will not be parsed. You can exclude for example tests from processing this way. This parameter is case sensitive and can be used multiple times. - -```--skip-doc-path ``` -```--skip-doc-prefix ``` - -Using this parameters you can tell ApiGen not to generate documentation for elements from certain files or with certain name prefix. Such classes will appear in class trees, but will not create a link to their documentation. These parameters are case sensitive and can be used multiple times. - -```--charset ``` - -Character set of source files, default is "auto" that lets ApiGen choose from all supported character sets. However if you use only one characters set across your source files you should set it explicitly to avoid autodetection because it can be tricky (and is not completely realiable). Moreover autodetection slows down the process of generating documentation. You can also use the parameter multiple times to provide a list of all used character sets in your documentation. In that case ApiGen will choose one of provided character sets for each file. - -```--main ``` - -Elements with this name prefix will be considered as the "main project" (the rest will be considered as libraries). - -```--title ``` - -Title of the generated documentation. - -```--base-url ``` - -Documentation base URL used in the sitemap. Only needed if you plan to make your documentation public. - -```--google-cse-id ``` - -If you have a Google CSE ID, the search box will use it when you do not enter an exact class, constant or function name. - -```--google-cse-label ``` - -This will be the default label when using Google CSE. - -```--google-analytics ``` - -A Google Analytics tracking code. If provided, an ansynchronous tracking code will be placed into every generated page. - -```--template-config ``` - -Template config file, default is the config file of ApiGen default template. - -```--allowed-html ``` - -List of allowed HTML tags in documentation separated by comma. Default value is "b,i,a,ul,ol,li,p,br,var,samp,kbd,tt". - -```--groups ``` - -How should elements be grouped in the menu. Possible options are "auto", "namespaces", "packages" and "none". Default value is "auto" (namespaces are used if the source code uses them, packages otherwise). - -```--autocomplete ``` - -List of element types that will appear in the search input autocomplete. Possible values are "classes", "constants", "functions", "methods", "properties" and "classconstants". Default value is "classes,constants,functions". - -```--access-levels ``` - -Access levels of methods and properties that should get their documentation parsed. Default value is "public,protected" (don't generate private class members). - -```--internal ``` - -Generate documentation for elements marked as internal (```@internal``` without description) and display parts of the documentation that are marked as internal (```@internal with description ...``` or inline ```{@internal ...}```), default is "No". - -```--php ``` - -Generate documentation for PHP internal classes, default is "Yes". - -```--tree ``` - -Generate tree view of classes, interfaces, traits and exceptions, default is "Yes". - -```--deprecated ``` - -Generate documentation for deprecated elements, default is "No". - -```--todo ``` - -Generate a list of tasks, default is "No". - -```--source-code ``` - -Generate highlighted source code for user defined elements, default is "Yes". - -```--download ``` - -Add a link to download documentation as a ZIP archive, default is "No". - -```--report ``` - -Save a checkstyle report of poorly documented elements into a file. - -```--wipeout ``` - -Delete files generated in the previous run, default is "Yes". - -```--quiet ``` - -Do not print any messages to the console, default is "No". - -```--progressbar ``` - -Display progressbars, default is "Yes". - -```--colors ``` - -Use colors, default "No" on Windows, "Yes" on other systems. Windows doesn't support colors in console however you can enable it with [Ansicon](http://adoxa.110mb.com/ansicon/). - -```--update-check ``` - -Check for a new version of ApiGen, default is "Yes". - -```--debug ``` - -Display additional information (exception trace) in case of an error, default is "No". - -```--help|-h ``` - -Display the list of possible options. - -Only ```--source``` and ```--destination``` parameters are required. You can provide them via command line or a configuration file. - -### Config files ### - -Instead of providing individual parameters via the command line, you can prepare a config file for later use. You can use all the above listed parameters (with one exception: the ```--config``` option) only without dashes and with an uppercase letter after each dash (so ```--access-level``` becomes ```accessLevel```). - -ApiGen uses the [NEON file format](http://ne-on.org) for all its config files. You can try the [online parser](http://ne-on.org) to debug your config files and see how they get parsed. - -Then you can call ApiGen with a single parameter ```--config``` specifying the config file to load. - -``` - apigen --config [options] -``` - -Even when using a config file, you can still provide additional parameters via the command line. Such parameters will have precedence over parameters from the config file. - -Keep in mind, that any values in the config file will be **overwritten** by values from the command line. That means that providing the ```--source``` parameter values both in the config file and via the command line will not result in using all the provided values but only those from the command line. - -If you provide no command line parameters at all, ApiGen will try to load a default config file called ```apigen.neon``` in the current working directory. If found it will work as if you used the ```--config``` option. Note that when using any command line option, you have to specify the config file if you have one. ApiGen will try to load one automatically only when no command line parameters are used. Option names have to be in camelCase in config files (```--template-config``` on the command line becomes ```templateConfig``` in a config file). You can see a full list of configuration options with short descriptions in the example config file [apigen.neon.example](https://github.com/apigen/apigen/blob/master/apigen.neon.example). - -### Example ### - -We are generating documentation for the Nella Framework. We want Nette and Doctrine to be parsed as well because we want their classes to appear in class trees, lists of parent classes and their members in lists of inherited properties, methods and constants. However we do not want to generate their full documentation along with highlighted source codes. And we do not want to process any "test" directories, because there might be classes that do not belong to the project actually. - -``` - apigen --source ~/nella/Nella --source ~/doctrine2/lib/Doctrine --source ~/doctrine2/lib/vendor --source ~/nette/Nette --skip-doc-path "~/doctrine2/*" --skip-doc-prefix Nette --exclude "*/tests/*" --destination ~/docs/ --title "Nella Framework" -``` - -## Requirements ## - -ApiGen requires PHP 5.3 or later. Four libraries it uses ([Nette](https://github.com/nette/nette), [Texy](https://github.com/dg/texy), [TokenReflection](https://github.com/Andrewsville/PHP-Token-Reflection) and [FSHL](https://github.com/kukulich/fshl)) require four additional PHP extensions: [tokenizer](http://php.net/manual/book.tokenizer.php), [mbstring](http://php.net/manual/book.mbstring.php), [iconv](http://php.net/manual/book.iconv.php) and [json](http://php.net/manual/book.json.php). For documenting PHAR archives you need the [phar extension](http://php.net/manual/book.phar.php) and for documenting gz or bz2 compressed PHARs, you need the [zlib](http://php.net/manual/book.zlib.php) or [bz2](http://php.net/manual/book.bzip2.php) extension respectively. To generate the ZIP file with documentation you need the [zip extension](http://php.net/manual/book.zip.php). - -When generating documentation of large libraries (Zend Framework for example) we recommend not to have the Xdebug PHP extension loaded (it does not need to be used, it significantly slows down the generating process even when only loaded). - -## Authors ## - -* [Jaroslav Hanslík](https://github.com/kukulich) -* [Ondřej Nešpor](https://github.com/Andrewsville) -* [David Grudl](https://github.com/dg) - -## Usage examples ## - -* [Doctrine](http://www.doctrine-project.org/api/orm/2.2/index.html) -* [Nette Framework](http://api.nette.org/2.0/) -* [TokenReflection library](http://andrewsville.github.com/PHP-Token-Reflection/) -* [FSHL library](http://fshl.kukulich.cz/api/) -* [Nella Framework](http://api.nellafw.org/) -* Jyxo PHP Libraries, both [namespaced](http://jyxo.github.com/php/) and [non-namespaced](http://jyxo.github.com/php-no-namespace/) - -Besides from these publicly visible examples there are companies that use ApiGen to generate their inhouse documentation: [Medio Interactive](http://www.medio.cz/), [Wikidi](http://wikidi.com/). \ No newline at end of file diff --git a/apigen/apigen.bat b/apigen/apigen.bat deleted file mode 100755 index 9be5228e5dc..00000000000 --- a/apigen/apigen.bat +++ /dev/null @@ -1,16 +0,0 @@ -@echo off -REM ApiGen 2.8.0 - API documentation generator for PHP 5.3+ -REM -REM Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -REM Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -REM Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -REM -REM For the full copyright and license information, please view -REM the file LICENCE.md that was distributed with this source code. -REM - -IF EXIST "@php_bin@" ( - "@php_bin@" "@bin_dir@\apigen" %* -) ELSE ( - "php.exe" "%~dp0apigen.php" %* -) diff --git a/apigen/apigen.neon.example b/apigen/apigen.neon.example deleted file mode 100644 index e5e0de7f2f4..00000000000 --- a/apigen/apigen.neon.example +++ /dev/null @@ -1,67 +0,0 @@ -# Source file or directory to parse -source: -# Directory where to save the generated documentation -destination: -# List of allowed file extensions -extensions: [php] -# Mask to exclude file or directory from processing -exclude: -# Don't generate documentation for classes from file or directory with this mask -skipDocPath: -# Don't generate documentation for classes with this name prefix -skipDocPrefix: -# Character set of source files -charset: auto -# Main project name prefix -main: - -# Title of generated documentation -title: -# Documentation base URL -baseUrl: -# Google Custom Search ID -googleCseId: -# Google Custom Search label -googleCseLabel: -# Google Analytics tracking code -googleAnalytics: -# Template config file -templateConfig: './templates/default/config.neon' -# Grouping of classes -groups: auto -# List of allowed HTML tags in documentation -allowedHtml: [b, i, a, ul, ol, li, p, br, var, samp, kbd, tt] -# Element types for search input autocomplete -autocomplete: [classes, constants, functions] - -# Generate documentation for methods and properties with given access level -accessLevels: [public, protected] -# Generate documentation for elements marked as internal and display internal documentation parts -internal: No -# Generate documentation for PHP internal classes -php: Yes -# Generate tree view of classes, interfaces and exceptions -tree: Yes -# Generate documentation for deprecated classes, methods, properties and constants -deprecated: No -# Generate documentation of tasks -todo: No -# Generate highlighted source code files -sourceCode: Yes -# Add a link to download documentation as a ZIP archive -download: No -# Save a checkstyle report of poorly documented elements into a file -report: - -# Wipe out the destination directory first -wipeout: Yes -# Don't display scanning and generating messages -quiet: No -# Display progressbars -progressbar: Yes -# Use colors -colors: No -# Check for update -updateCheck: Yes -# Display additional information in case of an error -debug: No diff --git a/apigen/apigen.php b/apigen/apigen.php deleted file mode 100644 index 2b4aacd0310..00000000000 --- a/apigen/apigen.php +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env php -processCliOptions($options); - $generator = new Generator($config); - - // Help - if ($config->isHelpRequested()) { - echo $generator->colorize($generator->getHeader()); - echo $generator->colorize($config->getHelp()); - die(); - } - - // Prepare configuration - $config->prepare(); - - if ($config->debug) { - Debugger::$onFatalError = array(); - Debugger::enable(Debugger::DEVELOPMENT, false); - } - - $generator->output($generator->getHeader()); - - // Check for update (only in production mode) - if ($config->updateCheck && !$config->debug) { - ini_set('default_socket_timeout', 5); - $latestVersion = @file_get_contents('http://pear.apigen.org/rest/r/apigen/latest.txt'); - if (false !== $latestVersion && version_compare(trim($latestVersion), Generator::VERSION, '>')) { - $generator->output(sprintf("New version @header@%s@c available\n\n", $latestVersion)); - } - } - - // Scan - if (count($config->source) > 1) { - $generator->output(sprintf("Scanning\n @value@%s@c\n", implode("\n ", $config->source))); - } else { - $generator->output(sprintf("Scanning @value@%s@c\n", $config->source[0])); - } - if (count($config->exclude) > 1) { - $generator->output(sprintf("Excluding\n @value@%s@c\n", implode("\n ", $config->exclude))); - } elseif (!empty($config->exclude)) { - $generator->output(sprintf("Excluding @value@%s@c\n", $config->exclude[0])); - } - - $parsed = $generator->parse(); - - if (count($parsed->errors) > 1) { - $generator->output(sprintf("@error@Found %d errors@c\n\n", count($parsed->errors))); - - $no = 1; - foreach ($parsed->errors as $e) { - - if ($e instanceof TokenReflection\Exception\ParseException) { - $generator->output(sprintf("@error@%d.@c The TokenReflection library threw an exception while parsing the file @value@%s@c.\n", $no, $e->getFileName())); - if ($config->debug) { - $generator->output("\nThis can have two reasons: a) the source code in the file is not valid or b) you have just found a bug in the TokenReflection library.\n\n"); - $generator->output("If the license allows it please send the whole file or at least the following fragment describing where exacly is the problem along with the backtrace to apigen@apigen.org. Thank you!\n\n"); - - $token = $e->getToken(); - $sender = $e->getSender(); - if (!empty($token)) { - $generator->output( - sprintf( - "The cause of the exception \"%s\" was the @value@%s@c token (line @count@%d@c) in following part of %s source code:\n\n", - $e->getMessage(), - $e->getTokenName(), - $e->getExceptionLine(), - $sender && $sender->getName() ? '@value@' . $sender->getPrettyName() . '@c' : 'the' - ) - ); - } else { - $generator->output( - sprintf( - "The exception \"%s\" was thrown when processing %s source code:\n\n", - $e->getMessage(), - $sender && $sender->getName() ? '@value@' . $sender->getPrettyName() . '@c' : 'the' - ) - ); - } - - $generator->output($e->getSourcePart(true) . "\n\nThe exception backtrace is following:\n\n" . $e->getTraceAsString() . "\n\n"); - } - } elseif ($e instanceof TokenReflection\Exception\FileProcessingException) { - $generator->output(sprintf("@error@%d.@c %s\n", $no, $e->getMessage())); - if ($config->debug) { - $generator->output("\n" . $e->getDetail() . "\n\n"); - } - } else { - $generator->output(sprintf("@error@%d.@c %s\n", $no, $e->getMessage())); - if ($config->debug) { - $trace = $e->getTraceAsString(); - while ($e = $e->getPrevious()) { - $generator->output(sprintf("\n%s", $e->getMessage())); - $trace = $e->getTraceAsString(); - } - $generator->output(sprintf("\n%s\n\n", $trace)); - } - } - - $no++; - } - - if (!$config->debug) { - $generator->output("\nEnable the debug mode (@option@--debug@c) to see more details.\n\n"); - } - } - - $generator->output(sprintf("Found @count@%d@c classes, @count@%d@c constants, @count@%d@c functions and other @count@%d@c used PHP internal classes\n", $parsed->classes, $parsed->constants, $parsed->functions, $parsed->internalClasses)); - $generator->output(sprintf("Documentation for @count@%d@c classes, @count@%d@c constants, @count@%d@c functions and other @count@%d@c used PHP internal classes will be generated\n", $parsed->documentedClasses, $parsed->documentedConstants, $parsed->documentedFunctions, $parsed->documentedInternalClasses)); - - // Generating - $generator->output(sprintf("Using template config file @value@%s@c\n", $config->templateConfig)); - - if ($config->wipeout && is_dir($config->destination)) { - $generator->output("Wiping out destination directory\n"); - if (!$generator->wipeOutDestination()) { - throw new \RuntimeException('Cannot wipe out destination directory'); - } - } - - $generator->output(sprintf("Generating to directory @value@%s@c\n", $config->destination)); - $skipping = array_merge($config->skipDocPath, $config->skipDocPrefix); - if (count($skipping) > 1) { - $generator->output(sprintf("Will not generate documentation for\n @value@%s@c\n", implode("\n ", $skipping))); - } elseif (!empty($skipping)) { - $generator->output(sprintf("Will not generate documentation for @value@%s@c\n", $skipping[0])); - } - $generator->generate(); - - // End - $end = new \DateTime(); - $interval = $end->diff($start); - $parts = array(); - if ($interval->h > 0) { - $parts[] = sprintf('@count@%d@c hours', $interval->h); - } - if ($interval->i > 0) { - $parts[] = sprintf('@count@%d@c min', $interval->i); - } - if ($interval->s > 0) { - $parts[] = sprintf('@count@%d@c sec', $interval->s); - } - if (empty($parts)) { - $parts[] = sprintf('@count@%d@c sec', 1); - } - - $duration = implode(' ', $parts); - $generator->output(sprintf("Done. Total time: %s, used: @count@%d@c MB RAM\n", $duration, round(memory_get_peak_usage(true) / 1024 / 1024))); - -} catch (ConfigException $e) { - // Configuration error - echo $generator->colorize($generator->getHeader() . sprintf("\n@error@%s@c\n\n", $e->getMessage()) . $config->getHelp()); - - die(2); -} catch (\Exception $e) { - // Everything else - if ($config->debug) { - do { - echo $generator->colorize(sprintf("\n%s(%d): @error@%s@c", $e->getFile(), $e->getLine(), $e->getMessage())); - $trace = $e->getTraceAsString(); - } while ($e = $e->getPrevious()); - - printf("\n\n%s\n", $trace); - } else { - echo $generator->colorize(sprintf("\n@error@%s@c\n", $e->getMessage())); - } - - die(1); -} \ No newline at end of file diff --git a/apigen/hook-docs.php b/apigen/hook-docs.php new file mode 100644 index 00000000000..7de5461a75d --- /dev/null +++ b/apigen/hook-docs.php @@ -0,0 +1,225 @@ +' . $hook . ''; + } + + public static function process_hooks() { + // If we have one, get the PHP files from it. + $template_files = self::get_files( '*.php', GLOB_MARK, '../templates/' ); + $template_files[] = '../includes/wc-template-functions.php'; + $template_files[] = '../includes/wc-template-hooks.php'; + + $shortcode_files = self::get_files( '*.php', GLOB_MARK, '../includes/shortcodes/' ); + $widget_files = self::get_files( '*.php', GLOB_MARK, '../includes/widgets/' ); + $admin_files = self::get_files( '*.php', GLOB_MARK, '../includes/admin/' ); + $class_files = self::get_files( '*.php', GLOB_MARK, '../includes/' ); + $other_files = array( + '../woocommerce.php' + ); + + self::$files_to_scan = array( + 'Template Hooks' => $template_files, + 'Shortcode Hooks' => $shortcode_files, + 'Widget Hooks' => $widget_files, + 'Class Hooks' => $class_files, + 'Admin Hooks' => $admin_files, + 'Other Hooks' => $other_files, + ); + + $scanned = array(); + + ob_start(); + + echo '
'; + echo '

Action and Filter Hook Reference

'; + + foreach ( self::$files_to_scan as $heading => $files ) { + self::$custom_hooks_found = array(); + + foreach ( $files as $f ) { + self::$current_file = basename( $f ); + $tokens = token_get_all( file_get_contents( $f ) ); + $token_type = false; + $current_class = ''; + $current_function = ''; + + if ( in_array( self::$current_file, $scanned ) ) { + continue; + } + + $scanned[] = self::$current_file; + + foreach ( $tokens as $index => $token ) { + if ( is_array( $token ) ) { + $trimmed_token_1 = trim( $token[1] ); + if ( T_CLASS == $token[0] ) { + $token_type = 'class'; + } elseif ( T_FUNCTION == $token[0] ) { + $token_type = 'function'; + } elseif ( 'do_action' === $token[1] ) { + $token_type = 'action'; + } elseif ( 'apply_filters' === $token[1] ) { + $token_type = 'filter'; + } elseif ( $token_type && ! empty( $trimmed_token_1 ) ) { + switch ( $token_type ) { + case 'class' : + $current_class = $token[1]; + break; + case 'function' : + $current_function = $token[1]; + break; + case 'filter' : + case 'action' : + $hook = trim( $token[1], "'" ); + $loop = 0; + + if ( '_' === substr( $hook, '-1', 1 ) ) { + $hook .= '{'; + $open = true; + // Keep adding to hook until we find a comma or colon + while ( 1 ) { + $loop ++; + $next_hook = trim( trim( is_string( $tokens[ $index + $loop ] ) ? $tokens[ $index + $loop ] : $tokens[ $index + $loop ][1], '"' ), "'" ); + + if ( in_array( $next_hook, array( '.', '{', '}', '"', "'", ' ' ) ) ) { + continue; + } + + $hook_first = substr( $next_hook, 0, 1 ); + $hook_last = substr( $next_hook, -1, 1 ); + + if ( in_array( $next_hook, array( ',', ';' ) ) ) { + if ( $open ) { + $hook .= '}'; + $open = false; + } + break; + } + + if ( '_' === $hook_first ) { + $next_hook = '}' . $next_hook; + $open = false; + } + + if ( '_' === $hook_last ) { + $next_hook .= '{'; + $open = true; + } + + $hook .= $next_hook; + } + } + + if ( isset( self::$custom_hooks_found[ $hook ] ) ) { + self::$custom_hooks_found[ $hook ]['file'][] = self::$current_file; + } else { + self::$custom_hooks_found[ $hook ] = array( + 'line' => $token[2], + 'class' => $current_class, + 'function' => $current_function, + 'file' => array( self::$current_file ), + 'type' => $token_type, + ); + } + break; + } + $token_type = false; + } + } + } + } + + foreach ( self::$custom_hooks_found as $hook => $details ) { + if ( ! strstr( $hook, 'woocommerce' ) && ! strstr( $hook, 'product' ) && ! strstr( $hook, 'wc_' ) ) { + unset( self::$custom_hooks_found[ $hook ] ); + } + } + + ksort( self::$custom_hooks_found ); + + if ( ! empty( self::$custom_hooks_found ) ) { + echo '

' . $heading . '

'; + + echo ''; + + foreach ( self::$custom_hooks_found as $hook => $details ) { + echo ' + + + + ' . "\n"; + } + + echo '
HookTypeFile(s)
' . self::get_hook_link( $hook, $details ) . '' . $details['type'] . '' . implode( ', ', array_unique( $details['file'] ) ) . '
'; + } + } + + echo '
- - - - - - - <?php echo htmlspecialchars($title) ?> - - - - - - - -
-
-
-

getCode() ? ' #' . $exception->getCode() : '') ?>

- -

getMessage()) ?> getMessage())) ?>" id="netteBsSearch">search►

-
- - - - - - - -
-

Caused by 2) ? '►' : '▼' ?>

- -
-
-

getCode() ? ' #' . $ex->getCode() : '')) ?>

- -

getMessage()) ?>

-
- - - - - - - -
-

- -
- -
- - - - - getTrace(); $expanded = NULL ?> - getFile(), $expandPath) === 0) { - foreach ($stack as $key => $row) { - if (isset($row['file']) && strpos($row['file'], $expandPath) !== 0) { $expanded = $key; break; } - } - } ?> - -
-

Source file

- -
-

File: getFile(), $ex->getLine()) ?>   Line: getLine() ?>

- getFile())): ?>getFile(), $ex->getLine(), 15, isset($ex->context) ? $ex->context : NULL) ?> -
- - - - - -
-

Call stack

- -
-
    - $row): ?> -
  1. - - - - - inner-code - - - ">source   - - - - - (">arguments ) -

    - - -
    "> - - getParameters(); - } catch (\Exception $e) { - $params = array(); - } - foreach ($row['args'] as $k => $v) { - echo '\n"; - } - ?> -
    ', htmlspecialchars(isset($params[$k]) ? '$' . $params[$k]->name : "#$k"), ''; - echo Helpers::clickableDump($v); - echo "
    -
    - - - - -
    id="netteBsSrc">
    - - -
  2. - -
-
- - - - - context) && is_array($ex->context)):?> -
-

Variables

- -
-
- - context as $k => $v) { - echo '\n"; - } - ?> -
$', htmlspecialchars($k), '', Helpers::clickableDump($v), "
-
-
- - - getPrevious()) || (isset($ex->previous) && $ex = $ex->previous)); ?> -
' ?> - - - - - - - -
-

- -
- -
- - - - -
-

Environment

- -
-

$_SERVER

-
- - $v) echo '\n"; - ?> -
', htmlspecialchars($k), '', Helpers::clickableDump($v), "
-
- - -

$_SESSION

-
- -

empty

- - - $v) echo '\n"; - ?> -
', htmlspecialchars($k), '', $k === '__NF' ? 'Nette Session' : Helpers::clickableDump($v), "
- -
- - - -

Nette Session

-
- - $v) echo '\n"; - ?> -
', htmlspecialchars($k), '', Helpers::clickableDump($v), "
-
- - - - -

Constants

-
- - $v) { - echo ''; - echo '\n"; - } - ?> -
', htmlspecialchars($k), '', Helpers::clickableDump($v), "
-
- - - -

Included files ()

-
- - \n"; - } - ?> -
', htmlspecialchars($v), "
-
- - -

Configuration options

-
- |.+$#s', '', ob_get_clean()) ?> -
-
- - - -
-

HTTP request

- -
- -

Headers

-
- - $v) echo '\n"; - ?> -
', htmlspecialchars($k), '', htmlspecialchars($v), "
-
- - - - -

$

- -

empty

- -
- - $v) echo '\n"; - ?> -
', htmlspecialchars($k), '', Helpers::clickableDump($v), "
-
- - -
- - - -
-

HTTP response

- -
-

Headers

- -
';
-			?>
- -

no headers

- -
- - - - -
-

- -
- -
- - - - -
    -
  • Report generated at
  • - -
  • - -
  • - -
  • PHP
  • -
  • -
  • (revision )
  • -
-
-
- - - - diff --git a/apigen/libs/Nette/Nette/Diagnostics/templates/error.phtml b/apigen/libs/Nette/Nette/Diagnostics/templates/error.phtml deleted file mode 100644 index 9b0f1d9daef..00000000000 --- a/apigen/libs/Nette/Nette/Diagnostics/templates/error.phtml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - -Server Error - -

Server Error

- -

We're sorry! The server encountered an internal error and was unable to complete your request. Please try again later.

- -

error 500

diff --git a/apigen/libs/Nette/Nette/Diagnostics/templates/netteQ.js b/apigen/libs/Nette/Nette/Diagnostics/templates/netteQ.js deleted file mode 100644 index 68cf3a45622..00000000000 --- a/apigen/libs/Nette/Nette/Diagnostics/templates/netteQ.js +++ /dev/null @@ -1,332 +0,0 @@ -/** - * NetteQ - * - * This file is part of the Nette Framework. - * Copyright (c) 2004, 2012 David Grudl (http://davidgrudl.com) - */ - -var Nette = Nette || {}; - -(function(){ - -// simple class builder -Nette.Class = function(def) { - var cl = def.constructor || function(){}, nm, __hasProp = Object.prototype.hasOwnProperty; - delete def.constructor; - - if (def.Extends) { - var foo = function() { this.constructor = cl; }; - foo.prototype = def.Extends.prototype; - cl.prototype = new foo(); - delete def.Extends; - } - - if (def.Static) { - for (nm in def.Static) { if (__hasProp.call(def.Static, nm)) cl[nm] = def.Static[nm]; } - delete def.Static; - } - - for (nm in def) { if (__hasProp.call(def, nm)) cl.prototype[nm] = def[nm]; } - return cl; -}; - - -// supported cross-browser selectors: #id | div | div.class | .class -Nette.Q = Nette.Class({ - - Static: { - factory: function(selector) { - return new Nette.Q(selector); - }, - - implement: function(methods) { - var nm, fn = Nette.Q.implement, prot = Nette.Q.prototype, __hasProp = Object.prototype.hasOwnProperty; - for (nm in methods) { - if (!__hasProp.call(methods, nm)) { - continue; - } - fn[nm] = methods[nm]; - prot[nm] = (function(nm){ - return function() { return this.each(fn[nm], arguments); }; - }(nm)); - } - } - }, - - constructor: function(selector) { - if (typeof selector === "string") { - selector = this._find(document, selector); - - } else if (!selector || selector.nodeType || selector.length === undefined || selector === window) { - selector = [selector]; - } - - for (var i = 0, len = selector.length; i < len; i++) { - if (selector[i]) { this[this.length++] = selector[i]; } - } - }, - - length: 0, - - find: function(selector) { - return new Nette.Q(this._find(this[0], selector)); - }, - - _find: function(context, selector) { - if (!context || !selector) { - return []; - - } else if (document.querySelectorAll) { - return context.querySelectorAll(selector); - - } else if (selector.charAt(0) === '#') { // #id - return [document.getElementById(selector.substring(1))]; - - } else { // div | div.class | .class - selector = selector.split('.'); - var elms = context.getElementsByTagName(selector[0] || '*'); - - if (selector[1]) { - var list = [], pattern = new RegExp('(^|\\s)' + selector[1] + '(\\s|$)'); - for (var i = 0, len = elms.length; i < len; i++) { - if (pattern.test(elms[i].className)) { list.push(elms[i]); } - } - return list; - } else { - return elms; - } - } - }, - - dom: function() { - return this[0]; - }, - - each: function(callback, args) { - for (var i = 0, res; i < this.length; i++) { - if ((res = callback.apply(this[i], args || [])) !== undefined) { return res; } - } - return this; - } -}); - - -var $ = Nette.Q.factory, fn = Nette.Q.implement; - -fn({ - // cross-browser event attach - bind: function(event, handler) { - if (document.addEventListener && (event === 'mouseenter' || event === 'mouseleave')) { // simulate mouseenter & mouseleave using mouseover & mouseout - var old = handler; - event = event === 'mouseenter' ? 'mouseover' : 'mouseout'; - handler = function(e) { - for (var target = e.relatedTarget; target; target = target.parentNode) { - if (target === this) { return; } // target must not be inside this - } - old.call(this, e); - }; - } - - var data = fn.data.call(this), - events = data.events = data.events || {}; // use own handler queue - - if (!events[event]) { - var el = this, // fixes 'this' in iE - handlers = events[event] = [], - generic = fn.bind.genericHandler = function(e) { // dont worry, 'e' is passed in IE - if (!e.target) { - e.target = e.srcElement; - } - if (!e.preventDefault) { - e.preventDefault = function() { e.returnValue = false; }; - } - if (!e.stopPropagation) { - e.stopPropagation = function() { e.cancelBubble = true; }; - } - e.stopImmediatePropagation = function() { this.stopPropagation(); i = handlers.length; }; - for (var i = 0; i < handlers.length; i++) { - handlers[i].call(el, e); - } - }; - - if (document.addEventListener) { // non-IE - this.addEventListener(event, generic, false); - } else if (document.attachEvent) { // IE < 9 - this.attachEvent('on' + event, generic); - } - } - - events[event].push(handler); - }, - - // adds class to element - addClass: function(className) { - this.className = this.className.replace(/^|\s+|$/g, ' ').replace(' '+className+' ', ' ') + ' ' + className; - }, - - // removes class from element - removeClass: function(className) { - this.className = this.className.replace(/^|\s+|$/g, ' ').replace(' '+className+' ', ' '); - }, - - // tests whether element has given class - hasClass: function(className) { - return this.className.replace(/^|\s+|$/g, ' ').indexOf(' '+className+' ') > -1; - }, - - show: function() { - var dsp = fn.show.display = fn.show.display || {}, tag = this.tagName; - if (!dsp[tag]) { - var el = document.body.appendChild(document.createElement(tag)); - dsp[tag] = fn.css.call(el, 'display'); - } - this.style.display = dsp[tag]; - }, - - hide: function() { - this.style.display = 'none'; - }, - - css: function(property) { - return this.currentStyle ? this.currentStyle[property] - : (window.getComputedStyle ? document.defaultView.getComputedStyle(this, null).getPropertyValue(property) : undefined); - }, - - data: function() { - return this.nette ? this.nette : this.nette = {}; - }, - - val: function() { - var i; - if (!this.nodeName) { // radio - for (i = 0, len = this.length; i < len; i++) { - if (this[i].checked) { return this[i].value; } - } - return null; - } - - if (this.nodeName.toLowerCase() === 'select') { - var index = this.selectedIndex, options = this.options; - - if (index < 0) { - return null; - - } else if (this.type === 'select-one') { - return options[index].value; - } - - for (i = 0, values = [], len = options.length; i < len; i++) { - if (options[i].selected) { values.push(options[i].value); } - } - return values; - } - - if (this.type === 'checkbox') { - return this.checked; - } - - return this.value.replace(/^\s+|\s+$/g, ''); - }, - - _trav: function(el, selector, fce) { - selector = selector.split('.'); - while (el && !(el.nodeType === 1 && - (!selector[0] || el.tagName.toLowerCase() === selector[0]) && - (!selector[1] || fn.hasClass.call(el, selector[1])))) { - el = el[fce]; - } - return $(el); - }, - - closest: function(selector) { - return fn._trav(this, selector, 'parentNode'); - }, - - prev: function(selector) { - return fn._trav(this.previousSibling, selector, 'previousSibling'); - }, - - next: function(selector) { - return fn._trav(this.nextSibling, selector, 'nextSibling'); - }, - - // returns total offset for element - offset: function(coords) { - var el = this, ofs = coords ? {left: -coords.left || 0, top: -coords.top || 0} : fn.position.call(el); - while (el = el.offsetParent) { ofs.left += el.offsetLeft; ofs.top += el.offsetTop; } - - if (coords) { - fn.position.call(this, {left: -ofs.left, top: -ofs.top}); - } else { - return ofs; - } - }, - - // returns current position or move to new position - position: function(coords) { - if (coords) { - if (this.nette && this.nette.onmove) { - this.nette.onmove.call(this, coords); - } - this.style.left = (coords.left || 0) + 'px'; - this.style.top = (coords.top || 0) + 'px'; - } else { - return {left: this.offsetLeft, top: this.offsetTop, width: this.offsetWidth, height: this.offsetHeight}; - } - }, - - // makes element draggable - draggable: function(options) { - var $el = $(this), dE = document.documentElement, started; - options = options || {}; - - $(options.handle || this).bind('mousedown', function(e) { - e.preventDefault(); - e.stopPropagation(); - - if (fn.draggable.binded) { // missed mouseup out of window? - return dE.onmouseup(e); - } - - var deltaX = $el[0].offsetLeft - e.clientX, deltaY = $el[0].offsetTop - e.clientY; - fn.draggable.binded = true; - started = false; - - dE.onmousemove = function(e) { - e = e || event; - if (!started) { - if (options.draggedClass) { - $el.addClass(options.draggedClass); - } - if (options.start) { - options.start(e, $el); - } - started = true; - } - $el.position({left: e.clientX + deltaX, top: e.clientY + deltaY}); - return false; - }; - - dE.onmouseup = function(e) { - if (started) { - if (options.draggedClass) { - $el.removeClass(options.draggedClass); - } - if (options.stop) { - options.stop(e || event, $el); - } - } - fn.draggable.binded = dE.onmousemove = dE.onmouseup = null; - return false; - }; - - }).bind('click', function(e) { - if (started) { - e.stopImmediatePropagation(); - preventClick = false; - } - }); - } -}); - -})(); diff --git a/apigen/libs/Nette/Nette/Forms/Container.php b/apigen/libs/Nette/Nette/Forms/Container.php deleted file mode 100644 index 3ce3bbf3275..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Container.php +++ /dev/null @@ -1,493 +0,0 @@ -getForm(FALSE); - if (!$form || !$form->isAnchored() || !$form->isSubmitted()) { - $this->setValues($values, $erase); - } - return $this; - } - - - - /** - * Fill-in with values. - * @param array|Traversable values used to fill the form - * @param bool erase other controls? - * @return Container provides a fluent interface - */ - public function setValues($values, $erase = FALSE) - { - if ($values instanceof \Traversable) { - $values = iterator_to_array($values); - - } elseif (!is_array($values)) { - throw new Nette\InvalidArgumentException("First parameter must be an array, " . gettype($values) ." given."); - } - - foreach ($this->getComponents() as $name => $control) { - if ($control instanceof IControl) { - if (array_key_exists($name, $values)) { - $control->setValue($values[$name]); - - } elseif ($erase) { - $control->setValue(NULL); - } - - } elseif ($control instanceof Container) { - if (array_key_exists($name, $values)) { - $control->setValues($values[$name], $erase); - - } elseif ($erase) { - $control->setValues(array(), $erase); - } - } - } - return $this; - } - - - - /** - * Returns the values submitted by the form. - * @param bool return values as an array? - * @return Nette\ArrayHash|array - */ - public function getValues($asArray = FALSE) - { - $values = $asArray ? array() : new Nette\ArrayHash; - foreach ($this->getComponents() as $name => $control) { - if ($control instanceof IControl && !$control->isDisabled() && !$control instanceof ISubmitterControl) { - $values[$name] = $control->getValue(); - - } elseif ($control instanceof Container) { - $values[$name] = $control->getValues($asArray); - } - } - return $values; - } - - - - /********************* validation ****************d*g**/ - - - - /** - * Is form valid? - * @return bool - */ - public function isValid() - { - if ($this->valid === NULL) { - $this->validate(); - } - return $this->valid; - } - - - - /** - * Performs the server side validation. - * @return void - */ - public function validate() - { - $this->valid = TRUE; - $this->onValidate($this); - foreach ($this->getControls() as $control) { - if (!$control->getRules()->validate()) { - $this->valid = FALSE; - } - } - } - - - - /********************* form building ****************d*g**/ - - - - /** - * @param ControlGroup - * @return Container provides a fluent interface - */ - public function setCurrentGroup(ControlGroup $group = NULL) - { - $this->currentGroup = $group; - return $this; - } - - - - /** - * Returns current group. - * @return ControlGroup - */ - public function getCurrentGroup() - { - return $this->currentGroup; - } - - - - /** - * Adds the specified component to the IContainer. - * @param IComponent - * @param string - * @param string - * @return Container provides a fluent interface - * @throws Nette\InvalidStateException - */ - public function addComponent(Nette\ComponentModel\IComponent $component, $name, $insertBefore = NULL) - { - parent::addComponent($component, $name, $insertBefore); - if ($this->currentGroup !== NULL && $component instanceof IControl) { - $this->currentGroup->add($component); - } - return $this; - } - - - - /** - * Iterates over all form controls. - * @return \ArrayIterator - */ - public function getControls() - { - return $this->getComponents(TRUE, 'Nette\Forms\IControl'); - } - - - - /** - * Returns form. - * @param bool throw exception if form doesn't exist? - * @return Form - */ - public function getForm($need = TRUE) - { - return $this->lookup('Nette\Forms\Form', $need); - } - - - - /********************* control factories ****************d*g**/ - - - - /** - * Adds single-line text input control to the form. - * @param string control name - * @param string label - * @param int width of the control - * @param int maximum number of characters the user may enter - * @return Nette\Forms\Controls\TextInput - */ - public function addText($name, $label = NULL, $cols = NULL, $maxLength = NULL) - { - return $this[$name] = new Controls\TextInput($label, $cols, $maxLength); - } - - - - /** - * Adds single-line text input control used for sensitive input such as passwords. - * @param string control name - * @param string label - * @param int width of the control - * @param int maximum number of characters the user may enter - * @return Nette\Forms\Controls\TextInput - */ - public function addPassword($name, $label = NULL, $cols = NULL, $maxLength = NULL) - { - $control = new Controls\TextInput($label, $cols, $maxLength); - $control->setType('password'); - return $this[$name] = $control; - } - - - - /** - * Adds multi-line text input control to the form. - * @param string control name - * @param string label - * @param int width of the control - * @param int height of the control in text lines - * @return Nette\Forms\Controls\TextArea - */ - public function addTextArea($name, $label = NULL, $cols = 40, $rows = 10) - { - return $this[$name] = new Controls\TextArea($label, $cols, $rows); - } - - - - /** - * Adds control that allows the user to upload files. - * @param string control name - * @param string label - * @return Nette\Forms\Controls\UploadControl - */ - public function addUpload($name, $label = NULL) - { - return $this[$name] = new Controls\UploadControl($label); - } - - - - /** - * Adds hidden form control used to store a non-displayed value. - * @param string control name - * @param mixed default value - * @return Nette\Forms\Controls\HiddenField - */ - public function addHidden($name, $default = NULL) - { - $control = new Controls\HiddenField; - $control->setDefaultValue($default); - return $this[$name] = $control; - } - - - - /** - * Adds check box control to the form. - * @param string control name - * @param string caption - * @return Nette\Forms\Controls\Checkbox - */ - public function addCheckbox($name, $caption = NULL) - { - return $this[$name] = new Controls\Checkbox($caption); - } - - - - /** - * Adds set of radio button controls to the form. - * @param string control name - * @param string label - * @param array options from which to choose - * @return Nette\Forms\Controls\RadioList - */ - public function addRadioList($name, $label = NULL, array $items = NULL) - { - return $this[$name] = new Controls\RadioList($label, $items); - } - - - - /** - * Adds select box control that allows single item selection. - * @param string control name - * @param string label - * @param array items from which to choose - * @param int number of rows that should be visible - * @return Nette\Forms\Controls\SelectBox - */ - public function addSelect($name, $label = NULL, array $items = NULL, $size = NULL) - { - return $this[$name] = new Controls\SelectBox($label, $items, $size); - } - - - - /** - * Adds select box control that allows multiple item selection. - * @param string control name - * @param string label - * @param array options from which to choose - * @param int number of rows that should be visible - * @return Nette\Forms\Controls\MultiSelectBox - */ - public function addMultiSelect($name, $label = NULL, array $items = NULL, $size = NULL) - { - return $this[$name] = new Controls\MultiSelectBox($label, $items, $size); - } - - - - /** - * Adds button used to submit form. - * @param string control name - * @param string caption - * @return Nette\Forms\Controls\SubmitButton - */ - public function addSubmit($name, $caption = NULL) - { - return $this[$name] = new Controls\SubmitButton($caption); - } - - - - /** - * Adds push buttons with no default behavior. - * @param string control name - * @param string caption - * @return Nette\Forms\Controls\Button - */ - public function addButton($name, $caption) - { - return $this[$name] = new Controls\Button($caption); - } - - - - /** - * Adds graphical button used to submit form. - * @param string control name - * @param string URI of the image - * @param string alternate text for the image - * @return Nette\Forms\Controls\ImageButton - */ - public function addImage($name, $src = NULL, $alt = NULL) - { - return $this[$name] = new Controls\ImageButton($src, $alt); - } - - - - /** - * Adds naming container to the form. - * @param string name - * @return Container - */ - public function addContainer($name) - { - $control = new Container; - $control->currentGroup = $this->currentGroup; - return $this[$name] = $control; - } - - - - /********************* interface \ArrayAccess ****************d*g**/ - - - - /** - * Adds the component to the container. - * @param string component name - * @param Nette\ComponentModel\IComponent - * @return void - */ - final public function offsetSet($name, $component) - { - $this->addComponent($component, $name); - } - - - - /** - * Returns component specified by name. Throws exception if component doesn't exist. - * @param string component name - * @return Nette\ComponentModel\IComponent - * @throws Nette\InvalidArgumentException - */ - final public function offsetGet($name) - { - return $this->getComponent($name, TRUE); - } - - - - /** - * Does component specified by name exists? - * @param string component name - * @return bool - */ - final public function offsetExists($name) - { - return $this->getComponent($name, FALSE) !== NULL; - } - - - - /** - * Removes component from the container. - * @param string component name - * @return void - */ - final public function offsetUnset($name) - { - $component = $this->getComponent($name, FALSE); - if ($component !== NULL) { - $this->removeComponent($component); - } - } - - - - /** - * Prevents cloning. - */ - final public function __clone() - { - throw new Nette\NotImplementedException('Form cloning is not supported yet.'); - } - - - - /********************* deprecated ****************d*g**/ - - /** @deprecated */ - function addFile($name, $label = NULL) - { - trigger_error(__METHOD__ . '() is deprecated; use addUpload() instead.', E_USER_WARNING); - return $this->addUpload($name, $label); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/ControlGroup.php b/apigen/libs/Nette/Nette/Forms/ControlGroup.php deleted file mode 100644 index 730d846e47d..00000000000 --- a/apigen/libs/Nette/Nette/Forms/ControlGroup.php +++ /dev/null @@ -1,124 +0,0 @@ -controls = new \SplObjectStorage; - } - - - - /** - * @return ControlGroup provides a fluent interface - */ - public function add() - { - foreach (func_get_args() as $num => $item) { - if ($item instanceof IControl) { - $this->controls->attach($item); - - } elseif ($item instanceof \Traversable || is_array($item)) { - foreach ($item as $control) { - $this->controls->attach($control); - } - - } else { - throw new Nette\InvalidArgumentException("Only IFormControl items are allowed, the #$num parameter is invalid."); - } - } - return $this; - } - - - - /** - * @return array IFormControl - */ - public function getControls() - { - return iterator_to_array($this->controls); - } - - - - /** - * Sets user-specific option. - * Options recognized by DefaultFormRenderer - * - 'label' - textual or Html object label - * - 'visual' - indicates visual group - * - 'container' - container as Html object - * - 'description' - textual or Html object description - * - 'embedNext' - describes how render next group - * - * @param string key - * @param mixed value - * @return ControlGroup provides a fluent interface - */ - public function setOption($key, $value) - { - if ($value === NULL) { - unset($this->options[$key]); - - } else { - $this->options[$key] = $value; - } - return $this; - } - - - - /** - * Returns user-specific option. - * @param string key - * @param mixed default value - * @return mixed - */ - final public function getOption($key, $default = NULL) - { - return isset($this->options[$key]) ? $this->options[$key] : $default; - } - - - - /** - * Returns user-specific options. - * @return array - */ - final public function getOptions() - { - return $this->options; - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/BaseControl.php b/apigen/libs/Nette/Nette/Forms/Controls/BaseControl.php deleted file mode 100644 index 665668abf94..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/BaseControl.php +++ /dev/null @@ -1,666 +0,0 @@ -monitor('Nette\Forms\Form'); - parent::__construct(); - $this->control = Html::el('input'); - $this->label = Html::el('label'); - $this->caption = $caption; - $this->rules = new Nette\Forms\Rules($this); - } - - - - /** - * This method will be called when the component becomes attached to Form. - * @param Nette\Forms\IComponent - * @return void - */ - protected function attached($form) - { - if (!$this->disabled && $form instanceof Form && $form->isAnchored() && $form->isSubmitted()) { - $this->htmlName = NULL; - $this->loadHttpData(); - } - } - - - - /** - * Returns form. - * @param bool throw exception if form doesn't exist? - * @return Nette\Forms\Form - */ - public function getForm($need = TRUE) - { - return $this->lookup('Nette\Forms\Form', $need); - } - - - - /** - * Returns HTML name of control. - * @return string - */ - public function getHtmlName() - { - if ($this->htmlName === NULL) { - $name = str_replace(self::NAME_SEPARATOR, '][', $this->lookupPath('Nette\Forms\Form'), $count); - if ($count) { - $name = substr_replace($name, '', strpos($name, ']'), 1) . ']'; - } - if (is_numeric($name) || in_array($name, array('attributes','children','elements','focus','length','reset','style','submit','onsubmit'))) { - $name .= '_'; - } - $this->htmlName = $name; - } - return $this->htmlName; - } - - - - /** - * Changes control's HTML id. - * @param string new ID, or FALSE or NULL - * @return BaseControl provides a fluent interface - */ - public function setHtmlId($id) - { - $this->htmlId = $id; - return $this; - } - - - - /** - * Returns control's HTML id. - * @return string - */ - public function getHtmlId() - { - if ($this->htmlId === FALSE) { - return NULL; - - } elseif ($this->htmlId === NULL) { - $this->htmlId = sprintf(self::$idMask, $this->getForm()->getName(), $this->lookupPath('Nette\Forms\Form')); - } - return $this->htmlId; - } - - - - /** - * Changes control's HTML attribute. - * @param string name - * @param mixed value - * @return BaseControl provides a fluent interface - */ - public function setAttribute($name, $value = TRUE) - { - $this->control->$name = $value; - return $this; - } - - - - /** - * Sets user-specific option. - * Options recognized by DefaultFormRenderer - * - 'description' - textual or Html object description - * - * @param string key - * @param mixed value - * @return BaseControl provides a fluent interface - */ - public function setOption($key, $value) - { - if ($value === NULL) { - unset($this->options[$key]); - - } else { - $this->options[$key] = $value; - } - return $this; - } - - - - /** - * Returns user-specific option. - * @param string key - * @param mixed default value - * @return mixed - */ - final public function getOption($key, $default = NULL) - { - return isset($this->options[$key]) ? $this->options[$key] : $default; - } - - - - /** - * Returns user-specific options. - * @return array - */ - final public function getOptions() - { - return $this->options; - } - - - - /********************* translator ****************d*g**/ - - - - /** - * Sets translate adapter. - * @param Nette\Localization\ITranslator - * @return BaseControl provides a fluent interface - */ - public function setTranslator(Nette\Localization\ITranslator $translator = NULL) - { - $this->translator = $translator; - return $this; - } - - - - /** - * Returns translate adapter. - * @return Nette\Localization\ITranslator|NULL - */ - final public function getTranslator() - { - if ($this->translator === TRUE) { - return $this->getForm(FALSE) ? $this->getForm()->getTranslator() : NULL; - } - return $this->translator; - } - - - - /** - * Returns translated string. - * @param string - * @param int plural count - * @return string - */ - public function translate($s, $count = NULL) - { - $translator = $this->getTranslator(); - return $translator === NULL || $s == NULL ? $s : $translator->translate($s, $count); // intentionally == - } - - - - /********************* interface IFormControl ****************d*g**/ - - - - /** - * Sets control's value. - * @param mixed - * @return BaseControl provides a fluent interface - */ - public function setValue($value) - { - $this->value = $value; - return $this; - } - - - - /** - * Returns control's value. - * @return mixed - */ - public function getValue() - { - return $this->value; - } - - - - /** - * Is control filled? - * @return bool - */ - public function isFilled() - { - return (string) $this->getValue() !== ''; // NULL, FALSE, '' ==> FALSE - } - - - - /** - * Sets control's default value. - * @param mixed - * @return BaseControl provides a fluent interface - */ - public function setDefaultValue($value) - { - $form = $this->getForm(FALSE); - if (!$form || !$form->isAnchored() || !$form->isSubmitted()) { - $this->setValue($value); - } - return $this; - } - - - - /** - * Loads HTTP data. - * @return void - */ - public function loadHttpData() - { - $path = explode('[', strtr(str_replace(array('[]', ']'), '', $this->getHtmlName()), '.', '_')); - $this->setValue(Nette\Utils\Arrays::get($this->getForm()->getHttpData(), $path, NULL)); - } - - - - /** - * Disables or enables control. - * @param bool - * @return BaseControl provides a fluent interface - */ - public function setDisabled($value = TRUE) - { - $this->disabled = (bool) $value; - return $this; - } - - - - /** - * Is control disabled? - * @return bool - */ - public function isDisabled() - { - return $this->disabled; - } - - - - /********************* rendering ****************d*g**/ - - - - /** - * Generates control's HTML element. - * @return Nette\Utils\Html - */ - public function getControl() - { - $this->setOption('rendered', TRUE); - - $control = clone $this->control; - $control->name = $this->getHtmlName(); - $control->disabled = $this->disabled; - $control->id = $this->getHtmlId(); - $control->required = $this->isRequired(); - - $rules = self::exportRules($this->rules); - $rules = substr(PHP_VERSION_ID >= 50400 ? json_encode($rules, JSON_UNESCAPED_UNICODE) : json_encode($rules), 1, -1); - $rules = preg_replace('#"([a-z0-9_]+)":#i', '$1:', $rules); - $rules = preg_replace('#(?data('nette-rules', $rules ? $rules : NULL); - - return $control; - } - - - - /** - * Generates label's HTML element. - * @param string - * @return Nette\Utils\Html - */ - public function getLabel($caption = NULL) - { - $label = clone $this->label; - $label->for = $this->getHtmlId(); - if ($caption !== NULL) { - $label->setText($this->translate($caption)); - - } elseif ($this->caption instanceof Html) { - $label->add($this->caption); - - } else { - $label->setText($this->translate($this->caption)); - } - return $label; - } - - - - /** - * Returns control's HTML element template. - * @return Nette\Utils\Html - */ - final public function getControlPrototype() - { - return $this->control; - } - - - - /** - * Returns label's HTML element template. - * @return Nette\Utils\Html - */ - final public function getLabelPrototype() - { - return $this->label; - } - - - - /********************* rules ****************d*g**/ - - - - /** - * Adds a validation rule. - * @param mixed rule type - * @param string message to display for invalid data - * @param mixed optional rule arguments - * @return BaseControl provides a fluent interface - */ - public function addRule($operation, $message = NULL, $arg = NULL) - { - $this->rules->addRule($operation, $message, $arg); - return $this; - } - - - - /** - * Adds a validation condition a returns new branch. - * @param mixed condition type - * @param mixed optional condition arguments - * @return Nette\Forms\Rules new branch - */ - public function addCondition($operation, $value = NULL) - { - return $this->rules->addCondition($operation, $value); - } - - - - /** - * Adds a validation condition based on another control a returns new branch. - * @param Nette\Forms\IControl form control - * @param mixed condition type - * @param mixed optional condition arguments - * @return Nette\Forms\Rules new branch - */ - public function addConditionOn(IControl $control, $operation, $value = NULL) - { - return $this->rules->addConditionOn($control, $operation, $value); - } - - - - /** - * @return Nette\Forms\Rules - */ - final public function getRules() - { - return $this->rules; - } - - - - /** - * Makes control mandatory. - * @param string error message - * @return BaseControl provides a fluent interface - */ - final public function setRequired($message = NULL) - { - return $this->addRule(Form::FILLED, $message); - } - - - - /** - * Is control mandatory? - * @return bool - */ - final public function isRequired() - { - foreach ($this->rules as $rule) { - if ($rule->type === Rule::VALIDATOR && !$rule->isNegative && $rule->operation === Form::FILLED) { - return TRUE; - } - } - return FALSE; - } - - - - /** - * @return array - */ - protected static function exportRules($rules) - { - $payload = array(); - foreach ($rules as $rule) { - if (!is_string($op = $rule->operation)) { - $op = new Nette\Callback($op); - if (!$op->isStatic()) { - continue; - } - } - if ($rule->type === Rule::VALIDATOR) { - $item = array('op' => ($rule->isNegative ? '~' : '') . $op, 'msg' => $rules->formatMessage($rule, FALSE)); - - } elseif ($rule->type === Rule::CONDITION) { - $item = array( - 'op' => ($rule->isNegative ? '~' : '') . $op, - 'rules' => self::exportRules($rule->subRules), - 'control' => $rule->control->getHtmlName() - ); - if ($rule->subRules->getToggles()) { - $item['toggle'] = $rule->subRules->getToggles(); - } - } - - if (is_array($rule->arg)) { - foreach ($rule->arg as $key => $value) { - $item['arg'][$key] = $value instanceof IControl ? (object) array('control' => $value->getHtmlName()) : $value; - } - } elseif ($rule->arg !== NULL) { - $item['arg'] = $rule->arg instanceof IControl ? (object) array('control' => $rule->arg->getHtmlName()) : $rule->arg; - } - - $payload[] = $item; - } - return $payload; - } - - - - /********************* validation ****************d*g**/ - - - - /** - * Equal validator: are control's value and second parameter equal? - * @param Nette\Forms\IControl - * @param mixed - * @return bool - */ - public static function validateEqual(IControl $control, $arg) - { - $value = $control->getValue(); - foreach ((is_array($value) ? $value : array($value)) as $val) { - foreach ((is_array($arg) ? $arg : array($arg)) as $item) { - if ((string) $val === (string) ($item instanceof IControl ? $item->value : $item)) { - return TRUE; - } - } - } - return FALSE; - } - - - - /** - * Filled validator: is control filled? - * @param Nette\Forms\IControl - * @return bool - */ - public static function validateFilled(IControl $control) - { - return $control->isFilled(); - } - - - - /** - * Valid validator: is control valid? - * @param Nette\Forms\IControl - * @return bool - */ - public static function validateValid(IControl $control) - { - return $control->rules->validate(TRUE); - } - - - - /** - * Adds error message to the list. - * @param string error message - * @return void - */ - public function addError($message) - { - if (!in_array($message, $this->errors, TRUE)) { - $this->errors[] = $message; - } - $this->getForm()->addError($message); - } - - - - /** - * Returns errors corresponding to control. - * @return array - */ - public function getErrors() - { - return $this->errors; - } - - - - /** - * @return bool - */ - public function hasErrors() - { - return (bool) $this->errors; - } - - - - /** - * @return void - */ - public function cleanErrors() - { - $this->errors = array(); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/Button.php b/apigen/libs/Nette/Nette/Forms/Controls/Button.php deleted file mode 100644 index aed3b48f8d8..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/Button.php +++ /dev/null @@ -1,60 +0,0 @@ -control->type = 'button'; - } - - - - /** - * Bypasses label generation. - * @return void - */ - public function getLabel($caption = NULL) - { - return NULL; - } - - - - /** - * Generates control's HTML element. - * @param string - * @return Nette\Utils\Html - */ - public function getControl($caption = NULL) - { - $control = parent::getControl(); - $control->value = $this->translate($caption === NULL ? $this->caption : $caption); - return $control; - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/Checkbox.php b/apigen/libs/Nette/Nette/Forms/Controls/Checkbox.php deleted file mode 100644 index 049bffde112..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/Checkbox.php +++ /dev/null @@ -1,60 +0,0 @@ -control->type = 'checkbox'; - $this->value = FALSE; - } - - - - /** - * Sets control's value. - * @param bool - * @return Checkbox provides a fluent interface - */ - public function setValue($value) - { - $this->value = is_scalar($value) ? (bool) $value : FALSE; - return $this; - } - - - - /** - * Generates control's HTML element. - * @return Nette\Utils\Html - */ - public function getControl() - { - return parent::getControl()->checked($this->value); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/HiddenField.php b/apigen/libs/Nette/Nette/Forms/Controls/HiddenField.php deleted file mode 100644 index 8ba7f932cb1..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/HiddenField.php +++ /dev/null @@ -1,75 +0,0 @@ -control->type = 'hidden'; - $this->value = (string) $forcedValue; - $this->forcedValue = $forcedValue; - } - - - - /** - * Bypasses label generation. - * @return void - */ - public function getLabel($caption = NULL) - { - return NULL; - } - - - - /** - * Sets control's value. - * @param string - * @return HiddenField provides a fluent interface - */ - public function setValue($value) - { - $this->value = is_scalar($value) ? (string) $value : ''; - return $this; - } - - - - /** - * Generates control's HTML element. - * @return Nette\Utils\Html - */ - public function getControl() - { - return parent::getControl() - ->value($this->forcedValue === NULL ? $this->value : $this->forcedValue) - ->data('nette-rules', NULL); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/ImageButton.php b/apigen/libs/Nette/Nette/Forms/Controls/ImageButton.php deleted file mode 100644 index 2f93ee9ae61..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/ImageButton.php +++ /dev/null @@ -1,63 +0,0 @@ -control->type = 'image'; - $this->control->src = $src; - $this->control->alt = $alt; - } - - - - /** - * Returns HTML name of control. - * @return string - */ - public function getHtmlName() - { - $name = parent::getHtmlName(); - return strpos($name, '[') === FALSE ? $name : $name . '[]'; - } - - - - /** - * Loads HTTP data. - * @return void - */ - public function loadHttpData() - { - $path = $this->getHtmlName(); // img_x or img['x'] - $path = explode('[', strtr(str_replace(']', '', strpos($path, '[') === FALSE ? $path . '.x' : substr($path, 0, -2)), '.', '_')); - $this->setValue(Nette\Utils\Arrays::get($this->getForm()->getHttpData(), $path, NULL)); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/MultiSelectBox.php b/apigen/libs/Nette/Nette/Forms/Controls/MultiSelectBox.php deleted file mode 100644 index 9c39de9887e..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/MultiSelectBox.php +++ /dev/null @@ -1,110 +0,0 @@ -getRawValue(), array_keys($this->allowed)); - } - - - - /** - * Returns selected keys (not checked). - * @return array - */ - public function getRawValue() - { - if (is_scalar($this->value)) { - return array($this->value); - - } else { - $res = array(); - foreach ((array) $this->value as $val) { - if (is_scalar($val)) { - $res[] = $val; - } - } - return $res; - } - } - - - - /** - * Returns selected values. - * @return array - */ - public function getSelectedItem() - { - return $this->areKeysUsed() - ? array_intersect_key($this->allowed, array_flip($this->getValue())) - : $this->getValue(); - } - - - - /** - * Returns HTML name of control. - * @return string - */ - public function getHtmlName() - { - return parent::getHtmlName() . '[]'; - } - - - - /** - * Generates control's HTML element. - * @return Nette\Utils\Html - */ - public function getControl() - { - return parent::getControl()->multiple(TRUE); - } - - - - /** - * Count/length validator. - * @param MultiSelectBox - * @param array min and max length pair - * @return bool - */ - public static function validateLength(MultiSelectBox $control, $range) - { - if (!is_array($range)) { - $range = array($range, $range); - } - $count = count($control->getSelectedItem()); - return ($range[0] === NULL || $count >= $range[0]) && ($range[1] === NULL || $count <= $range[1]); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/RadioList.php b/apigen/libs/Nette/Nette/Forms/Controls/RadioList.php deleted file mode 100644 index 4fb2fffe619..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/RadioList.php +++ /dev/null @@ -1,190 +0,0 @@ -control->type = 'radio'; - $this->container = Html::el(); - $this->separator = Html::el('br'); - if ($items !== NULL) { - $this->setItems($items); - } - } - - - - /** - * Returns selected radio value. - * @param bool - * @return mixed - */ - public function getValue($raw = FALSE) - { - return is_scalar($this->value) && ($raw || isset($this->items[$this->value])) ? $this->value : NULL; - } - - - - /** - * Has been any radio button selected? - * @return bool - */ - public function isFilled() - { - return $this->getValue() !== NULL; - } - - - - /** - * Sets options from which to choose. - * @param array - * @return RadioList provides a fluent interface - */ - public function setItems(array $items) - { - $this->items = $items; - return $this; - } - - - - /** - * Returns options from which to choose. - * @return array - */ - final public function getItems() - { - return $this->items; - } - - - - /** - * Returns separator HTML element template. - * @return Nette\Utils\Html - */ - final public function getSeparatorPrototype() - { - return $this->separator; - } - - - - /** - * Returns container HTML element template. - * @return Nette\Utils\Html - */ - final public function getContainerPrototype() - { - return $this->container; - } - - - - /** - * Generates control's HTML element. - * @param mixed - * @return Nette\Utils\Html - */ - public function getControl($key = NULL) - { - if ($key === NULL) { - $container = clone $this->container; - $separator = (string) $this->separator; - - } elseif (!isset($this->items[$key])) { - return NULL; - } - - $control = parent::getControl(); - $id = $control->id; - $counter = -1; - $value = $this->value === NULL ? NULL : (string) $this->getValue(); - $label = Html::el('label'); - - foreach ($this->items as $k => $val) { - $counter++; - if ($key !== NULL && (string) $key !== (string) $k) { - continue; - } - - $control->id = $label->for = $id . '-' . $counter; - $control->checked = (string) $k === $value; - $control->value = $k; - - if ($val instanceof Html) { - $label->setHtml($val); - } else { - $label->setText($this->translate((string) $val)); - } - - if ($key !== NULL) { - return Html::el()->add($control)->add($label); - } - - $container->add((string) $control . (string) $label . $separator); - $control->data('nette-rules', NULL); - // TODO: separator after last item? - } - - return $container; - } - - - - /** - * Generates label's HTML element. - * @param string - * @return void - */ - public function getLabel($caption = NULL) - { - $label = parent::getLabel($caption); - $label->for = NULL; - return $label; - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/SelectBox.php b/apigen/libs/Nette/Nette/Forms/Controls/SelectBox.php deleted file mode 100644 index 25634caf8df..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/SelectBox.php +++ /dev/null @@ -1,242 +0,0 @@ -control->setName('select'); - $this->control->size = $size > 1 ? (int) $size : NULL; - if ($items !== NULL) { - $this->setItems($items); - } - } - - - - /** - * Returns selected item key. - * @return mixed - */ - public function getValue() - { - return is_scalar($this->value) && isset($this->allowed[$this->value]) ? $this->value : NULL; - } - - - - /** - * Returns selected item key (not checked). - * @return mixed - */ - public function getRawValue() - { - return is_scalar($this->value) ? $this->value : NULL; - } - - - - /** - * Has been any item selected? - * @return bool - */ - public function isFilled() - { - $value = $this->getValue(); - return is_array($value) ? count($value) > 0 : $value !== NULL; - } - - - - /** - * Sets first prompt item in select box. - * @param string - * @return SelectBox provides a fluent interface - */ - public function setPrompt($prompt) - { - if ($prompt === TRUE) { // back compatibility - $prompt = reset($this->items); - unset($this->allowed[key($this->items)], $this->items[key($this->items)]); - } - $this->prompt = $prompt; - return $this; - } - - - - /** @deprecated */ - function skipFirst($v = NULL) - { - trigger_error(__METHOD__ . '() is deprecated; use setPrompt() instead.', E_USER_WARNING); - return $this->setPrompt($v); - } - - - - /** - * Returns first prompt item? - * @return mixed - */ - final public function getPrompt() - { - return $this->prompt; - } - - - - /** - * Are the keys used? - * @return bool - */ - final public function areKeysUsed() - { - return $this->useKeys; - } - - - - /** - * Sets items from which to choose. - * @param array - * @param bool - * @return SelectBox provides a fluent interface - */ - public function setItems(array $items, $useKeys = TRUE) - { - $allowed = array(); - foreach ($items as $k => $v) { - foreach ((is_array($v) ? $v : array($k => $v)) as $key => $value) { - if (!$useKeys) { - if (!is_scalar($value)) { - throw new Nette\InvalidArgumentException("All items must be scalar."); - } - $key = $value; - } - - if (isset($allowed[$key])) { - throw new Nette\InvalidArgumentException("Items contain duplication for key '$key'."); - } - - $allowed[$key] = $value; - } - } - - $this->items = $items; - $this->allowed = $allowed; - $this->useKeys = (bool) $useKeys; - return $this; - } - - - - /** - * Returns items from which to choose. - * @return array - */ - final public function getItems() - { - return $this->items; - } - - - - /** - * Returns selected value. - * @return string - */ - public function getSelectedItem() - { - $value = $this->getValue(); - return ($this->useKeys && $value !== NULL) ? $this->allowed[$value] : $value; - } - - - - /** - * Generates control's HTML element. - * @return Nette\Utils\Html - */ - public function getControl() - { - $selected = $this->getValue(); - $selected = is_array($selected) ? array_flip($selected) : array($selected => TRUE); - $control = parent::getControl(); - $option = Nette\Utils\Html::el('option'); - - if ($this->prompt !== FALSE) { - $control->add($this->prompt instanceof Nette\Utils\Html - ? $this->prompt->value('') - : (string) $option->value('')->setText($this->translate((string) $this->prompt)) - ); - } - - foreach ($this->items as $key => $value) { - if (!is_array($value)) { - $value = array($key => $value); - $dest = $control; - } else { - $dest = $control->create('optgroup')->label($this->translate($key)); - } - - foreach ($value as $key2 => $value2) { - if ($value2 instanceof Nette\Utils\Html) { - $dest->add((string) $value2->selected(isset($selected[$key2]))); - - } else { - $key2 = $this->useKeys ? $key2 : $value2; - $value2 = $this->translate((string) $value2); - $dest->add((string) $option->value($key2) - ->selected(isset($selected[$key2])) - ->setText($value2)); - } - } - } - return $control; - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/SubmitButton.php b/apigen/libs/Nette/Nette/Forms/Controls/SubmitButton.php deleted file mode 100644 index 8b36d17694c..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/SubmitButton.php +++ /dev/null @@ -1,123 +0,0 @@ -control->type = 'submit'; - } - - - - /** - * Sets 'pressed' indicator. - * @param bool - * @return SubmitButton provides a fluent interface - */ - public function setValue($value) - { - if ($this->value = $value !== NULL) { - $this->getForm()->setSubmittedBy($this); - } - return $this; - } - - - - /** - * Tells if the form was submitted by this button. - * @return bool - */ - public function isSubmittedBy() - { - return $this->getForm()->isSubmitted() === $this; - } - - - - /** - * Sets the validation scope. Clicking the button validates only the controls within the specified scope. - * @param mixed - * @return SubmitButton provides a fluent interface - */ - public function setValidationScope($scope) - { - // TODO: implement groups - $this->validationScope = (bool) $scope; - $this->control->formnovalidate = !$this->validationScope; - return $this; - } - - - - /** - * Gets the validation scope. - * @return mixed - */ - final public function getValidationScope() - { - return $this->validationScope; - } - - - - /** - * Fires click event. - * @return void - */ - public function click() - { - $this->onClick($this); - } - - - - /** - * Submitted validator: has been button pressed? - * @param Nette\Forms\ISubmitterControl - * @return bool - */ - public static function validateSubmitted(Nette\Forms\ISubmitterControl $control) - { - return $control->isSubmittedBy(); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/TextArea.php b/apigen/libs/Nette/Nette/Forms/Controls/TextArea.php deleted file mode 100644 index b0c68b1850d..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/TextArea.php +++ /dev/null @@ -1,53 +0,0 @@ -control->setName('textarea'); - $this->control->cols = $cols; - $this->control->rows = $rows; - $this->value = ''; - } - - - - /** - * Generates control's HTML element. - * @return Nette\Utils\Html - */ - public function getControl() - { - $control = parent::getControl(); - $control->setText($this->getValue() === '' ? $this->translate($this->emptyValue) : $this->value); - return $control; - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/TextBase.php b/apigen/libs/Nette/Nette/Forms/Controls/TextBase.php deleted file mode 100644 index 0bfee49515d..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/TextBase.php +++ /dev/null @@ -1,265 +0,0 @@ -value = is_array($value) ? '' : (string) $value; - return $this; - } - - - - /** - * Returns control's value. - * @return string - */ - public function getValue() - { - $value = $this->value; - foreach ($this->filters as $filter) { - $value = (string) $filter/*5.2*->invoke*/($value); - } - return $value === $this->translate($this->emptyValue) ? '' : $value; - } - - - - /** - * Sets the special value which is treated as empty string. - * @param string - * @return TextBase provides a fluent interface - */ - public function setEmptyValue($value) - { - $this->emptyValue = (string) $value; - return $this; - } - - - - /** - * Returns the special value which is treated as empty string. - * @return string - */ - final public function getEmptyValue() - { - return $this->emptyValue; - } - - - - /** - * Appends input string filter callback. - * @param callable - * @return TextBase provides a fluent interface - */ - public function addFilter($filter) - { - $this->filters[] = new Nette\Callback($filter); - return $this; - } - - - - public function getControl() - { - $control = parent::getControl(); - foreach ($this->getRules() as $rule) { - if ($rule->type === Nette\Forms\Rule::VALIDATOR && !$rule->isNegative - && ($rule->operation === Form::LENGTH || $rule->operation === Form::MAX_LENGTH) - ) { - $control->maxlength = is_array($rule->arg) ? $rule->arg[1] : $rule->arg; - } - } - if ($this->emptyValue !== '') { - $control->data('nette-empty-value', $this->translate($this->emptyValue)); - } - return $control; - } - - - - public function addRule($operation, $message = NULL, $arg = NULL) - { - if ($operation === Form::FLOAT) { - $this->addFilter(array(__CLASS__, 'filterFloat')); - } - return parent::addRule($operation, $message, $arg); - } - - - - /** - * Min-length validator: has control's value minimal length? - * @param TextBase - * @param int length - * @return bool - */ - public static function validateMinLength(TextBase $control, $length) - { - return Strings::length($control->getValue()) >= $length; - } - - - - /** - * Max-length validator: is control's value length in limit? - * @param TextBase - * @param int length - * @return bool - */ - public static function validateMaxLength(TextBase $control, $length) - { - return Strings::length($control->getValue()) <= $length; - } - - - - /** - * Length validator: is control's value length in range? - * @param TextBase - * @param array min and max length pair - * @return bool - */ - public static function validateLength(TextBase $control, $range) - { - if (!is_array($range)) { - $range = array($range, $range); - } - return Validators::isInRange(Strings::length($control->getValue()), $range); - } - - - - /** - * Email validator: is control's value valid email address? - * @param TextBase - * @return bool - */ - public static function validateEmail(TextBase $control) - { - return Validators::isEmail($control->getValue()); - } - - - - /** - * URL validator: is control's value valid URL? - * @param TextBase - * @return bool - */ - public static function validateUrl(TextBase $control) - { - return Validators::isUrl($control->getValue()) || Validators::isUrl('http://' . $control->getValue()); - } - - - - /** @deprecated */ - public static function validateRegexp(TextBase $control, $regexp) - { - return (bool) Strings::match($control->getValue(), $regexp); - } - - - - /** - * Regular expression validator: matches control's value regular expression? - * @param TextBase - * @param string - * @return bool - */ - public static function validatePattern(TextBase $control, $pattern) - { - return (bool) Strings::match($control->getValue(), "\x01^($pattern)$\x01u"); - } - - - - /** - * Integer validator: is a control's value decimal number? - * @param TextBase - * @return bool - */ - public static function validateInteger(TextBase $control) - { - return Validators::isNumericInt($control->getValue()); - } - - - - /** - * Float validator: is a control's value float number? - * @param TextBase - * @return bool - */ - public static function validateFloat(TextBase $control) - { - return Validators::isNumeric(static::filterFloat($control->getValue())); - } - - - - /** - * Rangle validator: is a control's value number in specified range? - * @param TextBase - * @param array min and max value pair - * @return bool - */ - public static function validateRange(TextBase $control, $range) - { - return Validators::isInRange($control->getValue(), $range); - } - - - - /** - * Float string cleanup. - * @param string - * @return string - */ - public static function filterFloat($s) - { - return str_replace(array(' ', ','), array('', '.'), $s); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/TextInput.php b/apigen/libs/Nette/Nette/Forms/Controls/TextInput.php deleted file mode 100644 index 46b672eb376..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/TextInput.php +++ /dev/null @@ -1,104 +0,0 @@ -control->type = 'text'; - $this->control->size = $cols; - $this->control->maxlength = $maxLength; - $this->addFilter($this->sanitize); - $this->value = ''; - } - - - - /** - * Filter: removes unnecessary whitespace and shortens value to control's max length. - * @return string - */ - public function sanitize($value) - { - if ($this->control->maxlength && Nette\Utils\Strings::length($value) > $this->control->maxlength) { - $value = Nette\Utils\Strings::substring($value, 0, $this->control->maxlength); - } - return Nette\Utils\Strings::trim(strtr($value, "\r\n", ' ')); - } - - - - /** - * Changes control's type attribute. - * @param string - * @return BaseControl provides a fluent interface - */ - public function setType($type) - { - $this->control->type = $type; - return $this; - } - - - - /** @deprecated */ - public function setPasswordMode($mode = TRUE) - { - $this->control->type = $mode ? 'password' : 'text'; - return $this; - } - - - - /** - * Generates control's HTML element. - * @return Nette\Utils\Html - */ - public function getControl() - { - $control = parent::getControl(); - foreach ($this->getRules() as $rule) { - if ($rule->isNegative || $rule->type !== Nette\Forms\Rule::VALIDATOR) { - - } elseif ($rule->operation === Nette\Forms\Form::RANGE && $control->type !== 'text') { - list($control->min, $control->max) = $rule->arg; - - } elseif ($rule->operation === Nette\Forms\Form::PATTERN) { - $control->pattern = $rule->arg; - } - } - if ($control->type !== 'password') { - $control->value = $this->getValue() === '' ? $this->translate($this->emptyValue) : $this->value; - } - return $control; - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Controls/UploadControl.php b/apigen/libs/Nette/Nette/Forms/Controls/UploadControl.php deleted file mode 100644 index 165a55327c7..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Controls/UploadControl.php +++ /dev/null @@ -1,138 +0,0 @@ -control->type = 'file'; - } - - - - /** - * This method will be called when the component (or component's parent) - * becomes attached to a monitored object. Do not call this method yourself. - * @param Nette\Forms\IComponent - * @return void - */ - protected function attached($form) - { - if ($form instanceof Nette\Forms\Form) { - if ($form->getMethod() !== Nette\Forms\Form::POST) { - throw new Nette\InvalidStateException('File upload requires method POST.'); - } - $form->getElementPrototype()->enctype = 'multipart/form-data'; - } - parent::attached($form); - } - - - - /** - * Sets control's value. - * @param array|Nette\Http\FileUpload - * @return Nette\Http\FileUpload provides a fluent interface - */ - public function setValue($value) - { - if (is_array($value)) { - $this->value = new Http\FileUpload($value); - - } elseif ($value instanceof Http\FileUpload) { - $this->value = $value; - - } else { - $this->value = new Http\FileUpload(NULL); - } - return $this; - } - - - - /** - * Has been any file uploaded? - * @return bool - */ - public function isFilled() - { - return $this->value instanceof Http\FileUpload && $this->value->isOK(); - } - - - - /** - * FileSize validator: is file size in limit? - * @param UploadControl - * @param int file size limit - * @return bool - */ - public static function validateFileSize(UploadControl $control, $limit) - { - $file = $control->getValue(); - return $file instanceof Http\FileUpload && $file->getSize() <= $limit; - } - - - - /** - * MimeType validator: has file specified mime type? - * @param UploadControl - * @param array|string mime type - * @return bool - */ - public static function validateMimeType(UploadControl $control, $mimeType) - { - $file = $control->getValue(); - if ($file instanceof Http\FileUpload) { - $type = strtolower($file->getContentType()); - $mimeTypes = is_array($mimeType) ? $mimeType : explode(',', $mimeType); - if (in_array($type, $mimeTypes, TRUE)) { - return TRUE; - } - if (in_array(preg_replace('#/.*#', '/*', $type), $mimeTypes, TRUE)) { - return TRUE; - } - } - return FALSE; - } - - - - /** - * Image validator: is file image? - * @param UploadControl - * @return bool - */ - public static function validateImage(UploadControl $control) - { - $file = $control->getValue(); - return $file instanceof Http\FileUpload && $file->isImage(); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Form.php b/apigen/libs/Nette/Nette/Forms/Form.php deleted file mode 100644 index 47f0c1832b0..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Form.php +++ /dev/null @@ -1,644 +0,0 @@ - element */ - private $element; - - /** @var IFormRenderer */ - private $renderer; - - /** @var Nette\Localization\ITranslator */ - private $translator; - - /** @var ControlGroup[] */ - private $groups = array(); - - /** @var array */ - private $errors = array(); - - - - /** - * Form constructor. - * @param string - */ - public function __construct($name = NULL) - { - $this->element = Nette\Utils\Html::el('form'); - $this->element->action = ''; // RFC 1808 -> empty uri means 'this' - $this->element->method = self::POST; - $this->element->id = $name === NULL ? NULL : 'frm-' . $name; - - $this->monitor(__CLASS__); - if ($name !== NULL) { - $tracker = new Controls\HiddenField($name); - $tracker->unmonitor(__CLASS__); - $this[self::TRACKER_ID] = $tracker; - } - parent::__construct(NULL, $name); - } - - - - /** - * This method will be called when the component (or component's parent) - * becomes attached to a monitored object. Do not call this method yourself. - * @param IComponent - * @return void - */ - protected function attached($obj) - { - if ($obj instanceof self) { - throw new Nette\InvalidStateException('Nested forms are forbidden.'); - } - } - - - - /** - * Returns self. - * @return Form - */ - final public function getForm($need = TRUE) - { - return $this; - } - - - - /** - * Sets form's action. - * @param mixed URI - * @return Form provides a fluent interface - */ - public function setAction($url) - { - $this->element->action = $url; - return $this; - } - - - - /** - * Returns form's action. - * @return mixed URI - */ - public function getAction() - { - return $this->element->action; - } - - - - /** - * Sets form's method. - * @param string get | post - * @return Form provides a fluent interface - */ - public function setMethod($method) - { - if ($this->httpData !== NULL) { - throw new Nette\InvalidStateException(__METHOD__ . '() must be called until the form is empty.'); - } - $this->element->method = strtolower($method); - return $this; - } - - - - /** - * Returns form's method. - * @return string get | post - */ - public function getMethod() - { - return $this->element->method; - } - - - - /** - * Cross-Site Request Forgery (CSRF) form protection. - * @param string - * @param int - * @return void - */ - public function addProtection($message = NULL, $timeout = NULL) - { - $session = $this->getSession()->getSection('Nette.Forms.Form/CSRF'); - $key = "key$timeout"; - if (isset($session->$key)) { - $token = $session->$key; - } else { - $session->$key = $token = Nette\Utils\Strings::random(); - } - $session->setExpiration($timeout, $key); - $this[self::PROTECTOR_ID] = new Controls\HiddenField($token); - $this[self::PROTECTOR_ID]->addRule(self::PROTECTION, $message, $token); - } - - - - /** - * Adds fieldset group to the form. - * @param string caption - * @param bool set this group as current - * @return ControlGroup - */ - public function addGroup($caption = NULL, $setAsCurrent = TRUE) - { - $group = new ControlGroup; - $group->setOption('label', $caption); - $group->setOption('visual', TRUE); - - if ($setAsCurrent) { - $this->setCurrentGroup($group); - } - - if (isset($this->groups[$caption])) { - return $this->groups[] = $group; - } else { - return $this->groups[$caption] = $group; - } - } - - - - /** - * Removes fieldset group from form. - * @param string|FormGroup - * @return void - */ - public function removeGroup($name) - { - if (is_string($name) && isset($this->groups[$name])) { - $group = $this->groups[$name]; - - } elseif ($name instanceof ControlGroup && in_array($name, $this->groups, TRUE)) { - $group = $name; - $name = array_search($group, $this->groups, TRUE); - - } else { - throw new Nette\InvalidArgumentException("Group not found in form '$this->name'"); - } - - foreach ($group->getControls() as $control) { - $this->removeComponent($control); - } - - unset($this->groups[$name]); - } - - - - /** - * Returns all defined groups. - * @return FormGroup[] - */ - public function getGroups() - { - return $this->groups; - } - - - - /** - * Returns the specified group. - * @param string name - * @return ControlGroup - */ - public function getGroup($name) - { - return isset($this->groups[$name]) ? $this->groups[$name] : NULL; - } - - - - /********************* translator ****************d*g**/ - - - - /** - * Sets translate adapter. - * @param Nette\Localization\ITranslator - * @return Form provides a fluent interface - */ - public function setTranslator(Nette\Localization\ITranslator $translator = NULL) - { - $this->translator = $translator; - return $this; - } - - - - /** - * Returns translate adapter. - * @return Nette\Localization\ITranslator|NULL - */ - final public function getTranslator() - { - return $this->translator; - } - - - - /********************* submission ****************d*g**/ - - - - /** - * Tells if the form is anchored. - * @return bool - */ - public function isAnchored() - { - return TRUE; - } - - - - /** - * Tells if the form was submitted. - * @return ISubmitterControl|FALSE submittor control - */ - final public function isSubmitted() - { - if ($this->submittedBy === NULL && count($this->getControls())) { - $this->getHttpData(); - $this->submittedBy = $this->httpData !== NULL; - } - return $this->submittedBy; - } - - - - /** - * Tells if the form was submitted and successfully validated. - * @return bool - */ - final public function isSuccess() - { - return $this->isSubmitted() && $this->isValid(); - } - - - - /** - * Sets the submittor control. - * @param ISubmitterControl - * @return Form provides a fluent interface - */ - public function setSubmittedBy(ISubmitterControl $by = NULL) - { - $this->submittedBy = $by === NULL ? FALSE : $by; - return $this; - } - - - - /** - * Returns submitted HTTP data. - * @return array - */ - final public function getHttpData() - { - if ($this->httpData === NULL) { - if (!$this->isAnchored()) { - throw new Nette\InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.'); - } - $this->httpData = $this->receiveHttpData(); - } - return $this->httpData; - } - - - - /** - * Fires submit/click events. - * @return void - */ - public function fireEvents() - { - if (!$this->isSubmitted()) { - return; - - } elseif ($this->submittedBy instanceof ISubmitterControl) { - if (!$this->submittedBy->getValidationScope() || $this->isValid()) { - $this->submittedBy->click(); - $valid = TRUE; - } else { - $this->submittedBy->onInvalidClick($this->submittedBy); - } - } - - if (isset($valid) || $this->isValid()) { - $this->onSuccess($this); - } else { - $this->onError($this); - if ($this->onInvalidSubmit) { - trigger_error(__CLASS__ . '->onInvalidSubmit is deprecated; use onError instead.', E_USER_WARNING); - $this->onInvalidSubmit($this); - } - } - - if ($this->onSuccess) { // back compatibility - $this->onSubmit($this); - } elseif ($this->onSubmit) { - trigger_error(__CLASS__ . '->onSubmit changed its behavior; use onSuccess instead.', E_USER_WARNING); - if (isset($valid) || $this->isValid()) { - $this->onSubmit($this); - } - } - } - - - - /** - * Internal: receives submitted HTTP data. - * @return array - */ - protected function receiveHttpData() - { - $httpRequest = $this->getHttpRequest(); - if (strcasecmp($this->getMethod(), $httpRequest->getMethod())) { - return; - } - - if ($httpRequest->isMethod('post')) { - $data = Nette\Utils\Arrays::mergeTree($httpRequest->getPost(), $httpRequest->getFiles()); - } else { - $data = $httpRequest->getQuery(); - } - - if ($tracker = $this->getComponent(self::TRACKER_ID, FALSE)) { - if (!isset($data[self::TRACKER_ID]) || $data[self::TRACKER_ID] !== $tracker->getValue()) { - return; - } - } - - return $data; - } - - - - /********************* data exchange ****************d*g**/ - - - - /** - * Returns the values submitted by the form. - * @return Nette\ArrayHash|array - */ - public function getValues($asArray = FALSE) - { - $values = parent::getValues($asArray); - unset($values[self::TRACKER_ID], $values[self::PROTECTOR_ID]); - return $values; - } - - - - /********************* validation ****************d*g**/ - - - - /** - * Adds error message to the list. - * @param string error message - * @return void - */ - public function addError($message) - { - $this->valid = FALSE; - if ($message !== NULL && !in_array($message, $this->errors, TRUE)) { - $this->errors[] = $message; - } - } - - - - /** - * Returns validation errors. - * @return array - */ - public function getErrors() - { - return $this->errors; - } - - - - /** - * @return bool - */ - public function hasErrors() - { - return (bool) $this->getErrors(); - } - - - - /** - * @return void - */ - public function cleanErrors() - { - $this->errors = array(); - $this->valid = NULL; - } - - - - /********************* rendering ****************d*g**/ - - - - /** - * Returns form's HTML element template. - * @return Nette\Utils\Html - */ - public function getElementPrototype() - { - return $this->element; - } - - - - /** - * Sets form renderer. - * @param IFormRenderer - * @return Form provides a fluent interface - */ - public function setRenderer(IFormRenderer $renderer) - { - $this->renderer = $renderer; - return $this; - } - - - - /** - * Returns form renderer. - * @return IFormRenderer - */ - final public function getRenderer() - { - if ($this->renderer === NULL) { - $this->renderer = new Rendering\DefaultFormRenderer; - } - return $this->renderer; - } - - - - /** - * Renders form. - * @return void - */ - public function render() - { - $args = func_get_args(); - array_unshift($args, $this); - echo call_user_func_array(array($this->getRenderer(), 'render'), $args); - } - - - - /** - * Renders form to string. - * @return bool can throw exceptions? (hidden parameter) - * @return string - */ - public function __toString() - { - try { - return $this->getRenderer()->render($this); - - } catch (\Exception $e) { - if (func_get_args() && func_get_arg(0)) { - throw $e; - } else { - Nette\Diagnostics\Debugger::toStringException($e); - } - } - } - - - - /********************* backend ****************d*g**/ - - - - /** - * @return Nette\Http\IRequest - */ - protected function getHttpRequest() - { - return Nette\Environment::getHttpRequest(); - } - - - - /** - * @return Nette\Http\Session - */ - protected function getSession() - { - return Nette\Environment::getSession(); - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/IControl.php b/apigen/libs/Nette/Nette/Forms/IControl.php deleted file mode 100644 index d6845f923f1..00000000000 --- a/apigen/libs/Nette/Nette/Forms/IControl.php +++ /dev/null @@ -1,70 +0,0 @@ - array( - 'container' => NULL, - 'errors' => TRUE, - ), - - 'error' => array( - 'container' => 'ul class=error', - 'item' => 'li', - ), - - 'group' => array( - 'container' => 'fieldset', - 'label' => 'legend', - 'description' => 'p', - ), - - 'controls' => array( - 'container' => 'table', - ), - - 'pair' => array( - 'container' => 'tr', - '.required' => 'required', - '.optional' => NULL, - '.odd' => NULL, - ), - - 'control' => array( - 'container' => 'td', - '.odd' => NULL, - - 'errors' => FALSE, - 'description' => 'small', - 'requiredsuffix' => '', - - '.required' => 'required', - '.text' => 'text', - '.password' => 'text', - '.file' => 'text', - '.submit' => 'button', - '.image' => 'imagebutton', - '.button' => 'button', - ), - - 'label' => array( - 'container' => 'th', - 'suffix' => NULL, - 'requiredsuffix' => '', - ), - - 'hidden' => array( - 'container' => 'div', - ), - ); - - /** @var Nette\Forms\Form */ - protected $form; - - /** @var int */ - protected $counter; - - - - /** - * Provides complete form rendering. - * @param Nette\Forms\Form - * @param string 'begin', 'errors', 'body', 'end' or empty to render all - * @return string - */ - public function render(Nette\Forms\Form $form, $mode = NULL) - { - if ($this->form !== $form) { - $this->form = $form; - $this->init(); - } - - $s = ''; - if (!$mode || $mode === 'begin') { - $s .= $this->renderBegin(); - } - if ((!$mode && $this->getValue('form errors')) || $mode === 'errors') { - $s .= $this->renderErrors(); - } - if (!$mode || $mode === 'body') { - $s .= $this->renderBody(); - } - if (!$mode || $mode === 'end') { - $s .= $this->renderEnd(); - } - return $s; - } - - - - /** @deprecated */ - public function setClientScript() - { - trigger_error(__METHOD__ . '() is deprecated; use unobstructive JavaScript instead.', E_USER_WARNING); - return $this; - } - - - - /** - * Initializes form. - * @return void - */ - protected function init() - { - // TODO: only for back compatiblity - remove? - $wrapper = & $this->wrappers['control']; - foreach ($this->form->getControls() as $control) { - if ($control->isRequired() && isset($wrapper['.required'])) { - $control->getLabelPrototype()->class($wrapper['.required'], TRUE); - } - - $el = $control->getControlPrototype(); - if ($el->getName() === 'input' && isset($wrapper['.' . $el->type])) { - $el->class($wrapper['.' . $el->type], TRUE); - } - } - } - - - - /** - * Renders form begin. - * @return string - */ - public function renderBegin() - { - $this->counter = 0; - - foreach ($this->form->getControls() as $control) { - $control->setOption('rendered', FALSE); - } - - if (strcasecmp($this->form->getMethod(), 'get') === 0) { - $el = clone $this->form->getElementPrototype(); - $url = explode('?', (string) $el->action, 2); - $el->action = $url[0]; - $s = ''; - if (isset($url[1])) { - foreach (preg_split('#[;&]#', $url[1]) as $param) { - $parts = explode('=', $param, 2); - $name = urldecode($parts[0]); - if (!isset($this->form[$name])) { - $s .= Html::el('input', array('type' => 'hidden', 'name' => $name, 'value' => urldecode($parts[1]))); - } - } - $s = "\n\t" . $this->getWrapper('hidden container')->setHtml($s); - } - return $el->startTag() . $s; - - - } else { - return $this->form->getElementPrototype()->startTag(); - } - } - - - - /** - * Renders form end. - * @return string - */ - public function renderEnd() - { - $s = ''; - foreach ($this->form->getControls() as $control) { - if ($control instanceof Nette\Forms\Controls\HiddenField && !$control->getOption('rendered')) { - $s .= (string) $control->getControl(); - } - } - if (iterator_count($this->form->getComponents(TRUE, 'Nette\Forms\Controls\TextInput')) < 2) { - $s .= ''; - } - if ($s) { - $s = $this->getWrapper('hidden container')->setHtml($s) . "\n"; - } - - return $s . $this->form->getElementPrototype()->endTag() . "\n"; - } - - - - /** - * Renders validation errors (per form or per control). - * @param Nette\Forms\IControl - * @return string - */ - public function renderErrors(Nette\Forms\IControl $control = NULL) - { - $errors = $control === NULL ? $this->form->getErrors() : $control->getErrors(); - if (count($errors)) { - $ul = $this->getWrapper('error container'); - $li = $this->getWrapper('error item'); - - foreach ($errors as $error) { - $item = clone $li; - if ($error instanceof Html) { - $item->add($error); - } else { - $item->setText($error); - } - $ul->add($item); - } - return "\n" . $ul->render(0); - } - } - - - - /** - * Renders form body. - * @return string - */ - public function renderBody() - { - $s = $remains = ''; - - $defaultContainer = $this->getWrapper('group container'); - $translator = $this->form->getTranslator(); - - foreach ($this->form->getGroups() as $group) { - if (!$group->getControls() || !$group->getOption('visual')) { - continue; - } - - $container = $group->getOption('container', $defaultContainer); - $container = $container instanceof Html ? clone $container : Html::el($container); - - $s .= "\n" . $container->startTag(); - - $text = $group->getOption('label'); - if ($text instanceof Html) { - $s .= $text; - - } elseif (is_string($text)) { - if ($translator !== NULL) { - $text = $translator->translate($text); - } - $s .= "\n" . $this->getWrapper('group label')->setText($text) . "\n"; - } - - $text = $group->getOption('description'); - if ($text instanceof Html) { - $s .= $text; - - } elseif (is_string($text)) { - if ($translator !== NULL) { - $text = $translator->translate($text); - } - $s .= $this->getWrapper('group description')->setText($text) . "\n"; - } - - $s .= $this->renderControls($group); - - $remains = $container->endTag() . "\n" . $remains; - if (!$group->getOption('embedNext')) { - $s .= $remains; - $remains = ''; - } - } - - $s .= $remains . $this->renderControls($this->form); - - $container = $this->getWrapper('form container'); - $container->setHtml($s); - return $container->render(0); - } - - - - /** - * Renders group of controls. - * @param Nette\Forms\Container|FormGroup - * @return string - */ - public function renderControls($parent) - { - if (!($parent instanceof Nette\Forms\Container || $parent instanceof Nette\Forms\ControlGroup)) { - throw new Nette\InvalidArgumentException("Argument must be FormContainer or FormGroup instance."); - } - - $container = $this->getWrapper('controls container'); - - $buttons = NULL; - foreach ($parent->getControls() as $control) { - if ($control->getOption('rendered') || $control instanceof Nette\Forms\Controls\HiddenField || $control->getForm(FALSE) !== $this->form) { - // skip - - } elseif ($control instanceof Nette\Forms\Controls\Button) { - $buttons[] = $control; - - } else { - if ($buttons) { - $container->add($this->renderPairMulti($buttons)); - $buttons = NULL; - } - $container->add($this->renderPair($control)); - } - } - - if ($buttons) { - $container->add($this->renderPairMulti($buttons)); - } - - $s = ''; - if (count($container)) { - $s .= "\n" . $container . "\n"; - } - - return $s; - } - - - - /** - * Renders single visual row. - * @param Nette\Forms\IControl - * @return string - */ - public function renderPair(Nette\Forms\IControl $control) - { - $pair = $this->getWrapper('pair container'); - $pair->add($this->renderLabel($control)); - $pair->add($this->renderControl($control)); - $pair->class($this->getValue($control->isRequired() ? 'pair .required' : 'pair .optional'), TRUE); - $pair->class($control->getOption('class'), TRUE); - if (++$this->counter % 2) { - $pair->class($this->getValue('pair .odd'), TRUE); - } - $pair->id = $control->getOption('id'); - return $pair->render(0); - } - - - - /** - * Renders single visual row of multiple controls. - * @param IFormControl[] - * @return string - */ - public function renderPairMulti(array $controls) - { - $s = array(); - foreach ($controls as $control) { - if (!$control instanceof Nette\Forms\IControl) { - throw new Nette\InvalidArgumentException("Argument must be array of IFormControl instances."); - } - $s[] = (string) $control->getControl(); - } - $pair = $this->getWrapper('pair container'); - $pair->add($this->renderLabel($control)); - $pair->add($this->getWrapper('control container')->setHtml(implode(" ", $s))); - return $pair->render(0); - } - - - - /** - * Renders 'label' part of visual row of controls. - * @param Nette\Forms\IControl - * @return string - */ - public function renderLabel(Nette\Forms\IControl $control) - { - $head = $this->getWrapper('label container'); - - if ($control instanceof Nette\Forms\Controls\Checkbox || $control instanceof Nette\Forms\Controls\Button) { - return $head->setHtml(($head->getName() === 'td' || $head->getName() === 'th') ? ' ' : ''); - - } else { - $label = $control->getLabel(); - $suffix = $this->getValue('label suffix') . ($control->isRequired() ? $this->getValue('label requiredsuffix') : ''); - if ($label instanceof Html) { - $label->setHtml($label->getHtml() . $suffix); - $suffix = ''; - } - return $head->setHtml((string) $label . $suffix); - } - } - - - - /** - * Renders 'control' part of visual row of controls. - * @param Nette\Forms\IControl - * @return string - */ - public function renderControl(Nette\Forms\IControl $control) - { - $body = $this->getWrapper('control container'); - if ($this->counter % 2) { - $body->class($this->getValue('control .odd'), TRUE); - } - - $description = $control->getOption('description'); - if ($description instanceof Html) { - $description = ' ' . $control->getOption('description'); - - } elseif (is_string($description)) { - $description = ' ' . $this->getWrapper('control description')->setText($control->translate($description)); - - } else { - $description = ''; - } - - if ($control->isRequired()) { - $description = $this->getValue('control requiredsuffix') . $description; - } - - if ($this->getValue('control errors')) { - $description .= $this->renderErrors($control); - } - - if ($control instanceof Nette\Forms\Controls\Checkbox || $control instanceof Nette\Forms\Controls\Button) { - return $body->setHtml((string) $control->getControl() . (string) $control->getLabel() . $description); - - } else { - return $body->setHtml((string) $control->getControl() . $description); - } - } - - - - /** - * @param string - * @return Nette\Utils\Html - */ - protected function getWrapper($name) - { - $data = $this->getValue($name); - return $data instanceof Html ? clone $data : Html::el($data); - } - - - - /** - * @param string - * @return string - */ - protected function getValue($name) - { - $name = explode(' ', $name); - $data = & $this->wrappers[$name[0]][$name[1]]; - return $data; - } - -} diff --git a/apigen/libs/Nette/Nette/Forms/Rule.php b/apigen/libs/Nette/Nette/Forms/Rule.php deleted file mode 100644 index 60ee09e8e1a..00000000000 --- a/apigen/libs/Nette/Nette/Forms/Rule.php +++ /dev/null @@ -1,55 +0,0 @@ - 'Security token did not match. Possible CSRF attack.', - Form::EQUAL => 'Please enter %s.', - Form::FILLED => 'Please complete mandatory field.', - Form::MIN_LENGTH => 'Please enter a value of at least %d characters.', - Form::MAX_LENGTH => 'Please enter a value no longer than %d characters.', - Form::LENGTH => 'Please enter a value between %d and %d characters long.', - Form::EMAIL => 'Please enter a valid email address.', - Form::URL => 'Please enter a valid URL.', - Form::INTEGER => 'Please enter a numeric value.', - Form::FLOAT => 'Please enter a numeric value.', - Form::RANGE => 'Please enter a value between %d and %d.', - Form::MAX_FILE_SIZE => 'The size of the uploaded file can be up to %d bytes.', - Form::IMAGE => 'The uploaded file must be image in format JPEG, GIF or PNG.', - ); - - /** @var Rule[] */ - private $rules = array(); - - /** @var Rules */ - private $parent; - - /** @var array */ - private $toggles = array(); - - /** @var IControl */ - private $control; - - - - public function __construct(IControl $control) - { - $this->control = $control; - } - - - - /** - * Adds a validation rule for the current control. - * @param mixed rule type - * @param string message to display for invalid data - * @param mixed optional rule arguments - * @return Rules provides a fluent interface - */ - public function addRule($operation, $message = NULL, $arg = NULL) - { - $rule = new Rule; - $rule->control = $this->control; - $rule->operation = $operation; - $this->adjustOperation($rule); - $rule->arg = $arg; - $rule->type = Rule::VALIDATOR; - if ($message === NULL && is_string($rule->operation) && isset(static::$defaultMessages[$rule->operation])) { - $rule->message = static::$defaultMessages[$rule->operation]; - } else { - $rule->message = $message; - } - $this->rules[] = $rule; - return $this; - } - - - - /** - * Adds a validation condition a returns new branch. - * @param mixed condition type - * @param mixed optional condition arguments - * @return Rules new branch - */ - public function addCondition($operation, $arg = NULL) - { - return $this->addConditionOn($this->control, $operation, $arg); - } - - - - /** - * Adds a validation condition on specified control a returns new branch. - * @param IControl form control - * @param mixed condition type - * @param mixed optional condition arguments - * @return Rules new branch - */ - public function addConditionOn(IControl $control, $operation, $arg = NULL) - { - $rule = new Rule; - $rule->control = $control; - $rule->operation = $operation; - $this->adjustOperation($rule); - $rule->arg = $arg; - $rule->type = Rule::CONDITION; - $rule->subRules = new static($this->control); - $rule->subRules->parent = $this; - - $this->rules[] = $rule; - return $rule->subRules; - } - - - - /** - * Adds a else statement. - * @return Rules else branch - */ - public function elseCondition() - { - $rule = clone end($this->parent->rules); - $rule->isNegative = !$rule->isNegative; - $rule->subRules = new static($this->parent->control); - $rule->subRules->parent = $this->parent; - $this->parent->rules[] = $rule; - return $rule->subRules; - } - - - - /** - * Ends current validation condition. - * @return Rules parent branch - */ - public function endCondition() - { - return $this->parent; - } - - - - /** - * Toggles HTML elememnt visibility. - * @param string element id - * @param bool hide element? - * @return Rules provides a fluent interface - */ - public function toggle($id, $hide = TRUE) - { - $this->toggles[$id] = $hide; - return $this; - } - - - - /** - * Validates against ruleset. - * @param bool stop before first error? - * @return bool is valid? - */ - public function validate($onlyCheck = FALSE) - { - foreach ($this->rules as $rule) { - if ($rule->control->isDisabled()) { - continue; - } - - $success = ($rule->isNegative xor $this->getCallback($rule)->invoke($rule->control, $rule->arg)); - - if ($rule->type === Rule::CONDITION && $success) { - if (!$rule->subRules->validate($onlyCheck)) { - return FALSE; - } - - } elseif ($rule->type === Rule::VALIDATOR && !$success) { - if (!$onlyCheck) { - $rule->control->addError(static::formatMessage($rule, TRUE)); - } - return FALSE; - } - } - return TRUE; - } - - - - /** - * Iterates over ruleset. - * @return \ArrayIterator - */ - final public function getIterator() - { - return new \ArrayIterator($this->rules); - } - - - - /** - * @return array - */ - final public function getToggles() - { - return $this->toggles; - } - - - - /** - * Process 'operation' string. - * @param Rule - * @return void - */ - private function adjustOperation($rule) - { - if (is_string($rule->operation) && ord($rule->operation[0]) > 127) { - $rule->isNegative = TRUE; - $rule->operation = ~$rule->operation; - } - - if (!$this->getCallback($rule)->isCallable()) { - $operation = is_scalar($rule->operation) ? " '$rule->operation'" : ''; - throw new Nette\InvalidArgumentException("Unknown operation$operation for control '{$rule->control->name}'."); - } - } - - - - private function getCallback($rule) - { - $op = $rule->operation; - if (is_string($op) && strncmp($op, ':', 1) === 0) { - return new Nette\Callback(get_class($rule->control), self::VALIDATE_PREFIX . ltrim($op, ':')); - } else { - return new Nette\Callback($op); - } - } - - - - public static function formatMessage($rule, $withValue) - { - $message = $rule->message; - if ($message instanceof Nette\Utils\Html) { - return $message; - } - if (!isset($message)) { // report missing message by notice - $message = static::$defaultMessages[$rule->operation]; - } - if ($translator = $rule->control->getForm()->getTranslator()) { - $message = $translator->translate($message, is_int($rule->arg) ? $rule->arg : NULL); - } - $message = vsprintf(preg_replace('#%(name|label|value)#', '%$0', $message), (array) $rule->arg); - $message = str_replace('%name', $rule->control->getName(), $message); - $message = str_replace('%label', $rule->control->translate($rule->control->caption), $message); - if ($withValue && strpos($message, '%value') !== FALSE) { - $message = str_replace('%value', $rule->control->getValue(), $message); - } - return $message; - } - -} diff --git a/apigen/libs/Nette/Nette/Http/Context.php b/apigen/libs/Nette/Nette/Http/Context.php deleted file mode 100644 index 1ea71bce697..00000000000 --- a/apigen/libs/Nette/Nette/Http/Context.php +++ /dev/null @@ -1,114 +0,0 @@ -request = $request; - $this->response = $response; - } - - - - /** - * Attempts to cache the sent entity by its last modification date. - * @param string|int|DateTime last modified time - * @param string strong entity tag validator - * @return bool - */ - public function isModified($lastModified = NULL, $etag = NULL) - { - if ($lastModified) { - $this->response->setHeader('Last-Modified', $this->response->date($lastModified)); - } - if ($etag) { - $this->response->setHeader('ETag', '"' . addslashes($etag) . '"'); - } - - $ifNoneMatch = $this->request->getHeader('If-None-Match'); - if ($ifNoneMatch === '*') { - $match = TRUE; // match, check if-modified-since - - } elseif ($ifNoneMatch !== NULL) { - $etag = $this->response->getHeader('ETag'); - - if ($etag == NULL || strpos(' ' . strtr($ifNoneMatch, ",\t", ' '), ' ' . $etag) === FALSE) { - return TRUE; - - } else { - $match = TRUE; // match, check if-modified-since - } - } - - $ifModifiedSince = $this->request->getHeader('If-Modified-Since'); - if ($ifModifiedSince !== NULL) { - $lastModified = $this->response->getHeader('Last-Modified'); - if ($lastModified != NULL && strtotime($lastModified) <= strtotime($ifModifiedSince)) { - $match = TRUE; - - } else { - return TRUE; - } - } - - if (empty($match)) { - return TRUE; - } - - $this->response->setCode(IResponse::S304_NOT_MODIFIED); - return FALSE; - } - - - - /** - * @return IRequest - */ - public function getRequest() - { - return $this->request; - } - - - - /** - * @return IResponse - */ - public function getResponse() - { - return $this->response; - } - -} diff --git a/apigen/libs/Nette/Nette/Http/FileUpload.php b/apigen/libs/Nette/Nette/Http/FileUpload.php deleted file mode 100644 index 480511fe206..00000000000 --- a/apigen/libs/Nette/Nette/Http/FileUpload.php +++ /dev/null @@ -1,222 +0,0 @@ -error = UPLOAD_ERR_NO_FILE; - return; // or throw exception? - } - } - $this->name = $value['name']; - $this->size = $value['size']; - $this->tmpName = $value['tmp_name']; - $this->error = $value['error']; - } - - - - /** - * Returns the file name. - * @return string - */ - public function getName() - { - return $this->name; - } - - - - /** - * Returns the sanitized file name. - * @return string - */ - public function getSanitizedName() - { - return trim(Nette\Utils\Strings::webalize($this->name, '.', FALSE), '.-'); - } - - - - /** - * Returns the MIME content type of an uploaded file. - * @return string - */ - public function getContentType() - { - if ($this->isOk() && $this->type === NULL) { - $this->type = Nette\Utils\MimeTypeDetector::fromFile($this->tmpName); - } - return $this->type; - } - - - - /** - * Returns the size of an uploaded file. - * @return int - */ - public function getSize() - { - return $this->size; - } - - - - /** - * Returns the path to an uploaded file. - * @return string - */ - public function getTemporaryFile() - { - return $this->tmpName; - } - - - - /** - * Returns the path to an uploaded file. - * @return string - */ - public function __toString() - { - return $this->tmpName; - } - - - - /** - * Returns the error code. {@link http://php.net/manual/en/features.file-upload.errors.php} - * @return int - */ - public function getError() - { - return $this->error; - } - - - - /** - * Is there any error? - * @return bool - */ - public function isOk() - { - return $this->error === UPLOAD_ERR_OK; - } - - - - /** - * Move uploaded file to new location. - * @param string - * @return FileUpload provides a fluent interface - */ - public function move($dest) - { - @mkdir(dirname($dest), 0777, TRUE); // @ - dir may already exist - /*5.2*if (substr(PHP_OS, 0, 3) === 'WIN') { @unlink($dest); }*/ - if (!call_user_func(is_uploaded_file($this->tmpName) ? 'move_uploaded_file' : 'rename', $this->tmpName, $dest)) { - throw new Nette\InvalidStateException("Unable to move uploaded file '$this->tmpName' to '$dest'."); - } - chmod($dest, 0666); - $this->tmpName = $dest; - return $this; - } - - - - /** - * Is uploaded file GIF, PNG or JPEG? - * @return bool - */ - public function isImage() - { - return in_array($this->getContentType(), array('image/gif', 'image/png', 'image/jpeg'), TRUE); - } - - - - /** - * Returns the image. - * @return Nette\Image - */ - public function toImage() - { - return Nette\Image::fromFile($this->tmpName); - } - - - - /** - * Returns the dimensions of an uploaded image as array. - * @return array - */ - public function getImageSize() - { - return $this->isOk() ? @getimagesize($this->tmpName) : NULL; // @ - files smaller than 12 bytes causes read error - } - - - - /** - * Get file contents. - * @return string - */ - public function getContents() - { - // future implementation can try to work around safe_mode and open_basedir limitations - return $this->isOk() ? file_get_contents($this->tmpName) : NULL; - } - -} diff --git a/apigen/libs/Nette/Nette/Http/IRequest.php b/apigen/libs/Nette/Nette/Http/IRequest.php deleted file mode 100644 index 0ccb12316a3..00000000000 --- a/apigen/libs/Nette/Nette/Http/IRequest.php +++ /dev/null @@ -1,140 +0,0 @@ -url = $url; - $this->url->freeze(); - if ($query === NULL) { - parse_str($url->query, $this->query); - } else { - $this->query = (array) $query; - } - $this->post = (array) $post; - $this->files = (array) $files; - $this->cookies = (array) $cookies; - $this->headers = (array) $headers; - $this->method = $method; - $this->remoteAddress = $remoteAddress; - $this->remoteHost = $remoteHost; - } - - - - /** - * Returns URL object. - * @return UrlScript - */ - final public function getUrl() - { - return $this->url; - } - - - - /** @deprecated */ - function getUri() - { - trigger_error(__METHOD__ . '() is deprecated; use ' . __CLASS__ . '::getUrl() instead.', E_USER_WARNING); - return $this->getUrl(); - } - - - - /********************* query, post, files & cookies ****************d*g**/ - - - - /** - * Returns variable provided to the script via URL query ($_GET). - * If no key is passed, returns the entire array. - * @param string key - * @param mixed default value - * @return mixed - */ - final public function getQuery($key = NULL, $default = NULL) - { - if (func_num_args() === 0) { - return $this->query; - - } elseif (isset($this->query[$key])) { - return $this->query[$key]; - - } else { - return $default; - } - } - - - - /** - * Returns variable provided to the script via POST method ($_POST). - * If no key is passed, returns the entire array. - * @param string key - * @param mixed default value - * @return mixed - */ - final public function getPost($key = NULL, $default = NULL) - { - if (func_num_args() === 0) { - return $this->post; - - } elseif (isset($this->post[$key])) { - return $this->post[$key]; - - } else { - return $default; - } - } - - - - /** - * Returns uploaded file. - * @param string key (or more keys) - * @return FileUpload - */ - final public function getFile($key) - { - $args = func_get_args(); - return Nette\Utils\Arrays::get($this->files, $args, NULL); - } - - - - /** - * Returns uploaded files. - * @return array - */ - final public function getFiles() - { - return $this->files; - } - - - - /** - * Returns variable provided to the script via HTTP cookies. - * @param string key - * @param mixed default value - * @return mixed - */ - final public function getCookie($key, $default = NULL) - { - if (func_num_args() === 0) { - return $this->cookies; - - } elseif (isset($this->cookies[$key])) { - return $this->cookies[$key]; - - } else { - return $default; - } - } - - - - /** - * Returns variables provided to the script via HTTP cookies. - * @return array - */ - final public function getCookies() - { - return $this->cookies; - } - - - - /********************* method & headers ****************d*g**/ - - - - /** - * Returns HTTP request method (GET, POST, HEAD, PUT, ...). The method is case-sensitive. - * @return string - */ - public function getMethod() - { - return $this->method; - } - - - - /** - * Checks if the request method is the given one. - * @param string - * @return bool - */ - public function isMethod($method) - { - return strcasecmp($this->method, $method) === 0; - } - - - - /** - * Checks if the request method is POST. - * @return bool - */ - public function isPost() - { - return $this->isMethod('POST'); - } - - - - /** - * Return the value of the HTTP header. Pass the header name as the - * plain, HTTP-specified header name (e.g. 'Accept-Encoding'). - * @param string - * @param mixed - * @return mixed - */ - final public function getHeader($header, $default = NULL) - { - $header = strtolower($header); - if (isset($this->headers[$header])) { - return $this->headers[$header]; - } else { - return $default; - } - } - - - - /** - * Returns all HTTP headers. - * @return array - */ - public function getHeaders() - { - return $this->headers; - } - - - - /** - * Returns referrer. - * @return Url|NULL - */ - final public function getReferer() - { - return isset($this->headers['referer']) ? new Url($this->headers['referer']) : NULL; - } - - - - /** - * Is the request is sent via secure channel (https). - * @return bool - */ - public function isSecured() - { - return $this->url->scheme === 'https'; - } - - - - /** - * Is AJAX request? - * @return bool - */ - public function isAjax() - { - return $this->getHeader('X-Requested-With') === 'XMLHttpRequest'; - } - - - - /** - * Returns the IP address of the remote client. - * @return string - */ - public function getRemoteAddress() - { - return $this->remoteAddress; - } - - - - /** - * Returns the host of the remote client. - * @return string - */ - public function getRemoteHost() - { - if (!$this->remoteHost) { - $this->remoteHost = $this->remoteAddress ? getHostByAddr($this->remoteAddress) : NULL; - } - return $this->remoteHost; - } - - - - /** - * Parse Accept-Language header and returns prefered language. - * @param array Supported languages - * @return string - */ - public function detectLanguage(array $langs) - { - $header = $this->getHeader('Accept-Language'); - if (!$header) { - return NULL; - } - - $s = strtolower($header); // case insensitive - $s = strtr($s, '_', '-'); // cs_CZ means cs-CZ - rsort($langs); // first more specific - preg_match_all('#(' . implode('|', $langs) . ')(?:-[^\s,;=]+)?\s*(?:;\s*q=([0-9.]+))?#', $s, $matches); - - if (!$matches[0]) { - return NULL; - } - - $max = 0; - $lang = NULL; - foreach ($matches[1] as $key => $value) { - $q = $matches[2][$key] === '' ? 1.0 : (float) $matches[2][$key]; - if ($q > $max) { - $max = $q; $lang = $value; - } - } - - return $lang; - } - -} diff --git a/apigen/libs/Nette/Nette/Http/RequestFactory.php b/apigen/libs/Nette/Nette/Http/RequestFactory.php deleted file mode 100644 index 96d10a88976..00000000000 --- a/apigen/libs/Nette/Nette/Http/RequestFactory.php +++ /dev/null @@ -1,252 +0,0 @@ - array('#/{2,}#' => '/'), // '%20' => '' - 'url' => array(), // '#[.,)]$#' => '' - ); - - /** @var string */ - private $encoding; - - - - /** - * @param string - * @return RequestFactory provides a fluent interface - */ - public function setEncoding($encoding) - { - $this->encoding = $encoding; - return $this; - } - - - - /** - * Creates current HttpRequest object. - * @return Request - */ - public function createHttpRequest() - { - // DETECTS URI, base path and script path of the request. - $url = new UrlScript; - $url->scheme = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https' : 'http'; - $url->user = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : ''; - $url->password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; - - // host & port - if (isset($_SERVER['HTTP_HOST'])) { - $pair = explode(':', $_SERVER['HTTP_HOST']); - - } elseif (isset($_SERVER['SERVER_NAME'])) { - $pair = explode(':', $_SERVER['SERVER_NAME']); - - } else { - $pair = array(''); - } - - $url->host = preg_match('#^[-._a-z0-9]+$#', $pair[0]) ? $pair[0] : ''; - - if (isset($pair[1])) { - $url->port = (int) $pair[1]; - - } elseif (isset($_SERVER['SERVER_PORT'])) { - $url->port = (int) $_SERVER['SERVER_PORT']; - } - - // path & query - if (isset($_SERVER['REQUEST_URI'])) { // Apache, IIS 6.0 - $requestUrl = $_SERVER['REQUEST_URI']; - - } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0 (PHP as CGI ?) - $requestUrl = $_SERVER['ORIG_PATH_INFO']; - if (isset($_SERVER['QUERY_STRING']) && $_SERVER['QUERY_STRING'] != '') { - $requestUrl .= '?' . $_SERVER['QUERY_STRING']; - } - } else { - $requestUrl = ''; - } - - $requestUrl = Strings::replace($requestUrl, $this->urlFilters['url']); - $tmp = explode('?', $requestUrl, 2); - $url->path = Strings::replace($tmp[0], $this->urlFilters['path']); - $url->query = isset($tmp[1]) ? $tmp[1] : ''; - - // normalized url - $url->canonicalize(); - $url->path = Strings::fixEncoding($url->path); - - // detect script path - if (isset($_SERVER['SCRIPT_NAME'])) { - $script = $_SERVER['SCRIPT_NAME']; - } elseif (isset($_SERVER['DOCUMENT_ROOT'], $_SERVER['SCRIPT_FILENAME']) - && strncmp($_SERVER['DOCUMENT_ROOT'], $_SERVER['SCRIPT_FILENAME'], strlen($_SERVER['DOCUMENT_ROOT'])) === 0 - ) { - $script = '/' . ltrim(strtr(substr($_SERVER['SCRIPT_FILENAME'], strlen($_SERVER['DOCUMENT_ROOT'])), '\\', '/'), '/'); - } else { - $script = '/'; - } - - $path = strtolower($url->path) . '/'; - $script = strtolower($script) . '/'; - $max = min(strlen($path), strlen($script)); - for ($i = 0; $i < $max; $i++) { - if ($path[$i] !== $script[$i]) { - break; - } elseif ($path[$i] === '/') { - $url->scriptPath = substr($url->path, 0, $i + 1); - } - } - - // GET, POST, COOKIE - $useFilter = (!in_array(ini_get('filter.default'), array('', 'unsafe_raw')) || ini_get('filter.default_flags')); - - parse_str($url->query, $query); - if (!$query) { - $query = $useFilter ? filter_input_array(INPUT_GET, FILTER_UNSAFE_RAW) : (empty($_GET) ? array() : $_GET); - } - $post = $useFilter ? filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW) : (empty($_POST) ? array() : $_POST); - $cookies = $useFilter ? filter_input_array(INPUT_COOKIE, FILTER_UNSAFE_RAW) : (empty($_COOKIE) ? array() : $_COOKIE); - - $gpc = (bool) get_magic_quotes_gpc(); - $old = error_reporting(error_reporting() ^ E_NOTICE); - - // remove fucking quotes and check (and optionally convert) encoding - if ($gpc || $this->encoding) { - $utf = strcasecmp($this->encoding, 'UTF-8') === 0; - $list = array(& $query, & $post, & $cookies); - while (list($key, $val) = each($list)) { - foreach ($val as $k => $v) { - unset($list[$key][$k]); - - if ($gpc) { - $k = stripslashes($k); - } - - if ($this->encoding && is_string($k) && (preg_match(self::NONCHARS, $k) || preg_last_error())) { - // invalid key -> ignore - - } elseif (is_array($v)) { - $list[$key][$k] = $v; - $list[] = & $list[$key][$k]; - - } else { - if ($gpc && !$useFilter) { - $v = stripSlashes($v); - } - if ($this->encoding) { - if ($utf) { - $v = Strings::fixEncoding($v); - - } else { - if (!Strings::checkEncoding($v)) { - $v = iconv($this->encoding, 'UTF-8//IGNORE', $v); - } - $v = html_entity_decode($v, ENT_QUOTES, 'UTF-8'); - } - $v = preg_replace(self::NONCHARS, '', $v); - } - $list[$key][$k] = $v; - } - } - } - unset($list, $key, $val, $k, $v); - } - - - // FILES and create FileUpload objects - $files = array(); - $list = array(); - if (!empty($_FILES)) { - foreach ($_FILES as $k => $v) { - if ($this->encoding && is_string($k) && (preg_match(self::NONCHARS, $k) || preg_last_error())) { - continue; - } - $v['@'] = & $files[$k]; - $list[] = $v; - } - } - - while (list(, $v) = each($list)) { - if (!isset($v['name'])) { - continue; - - } elseif (!is_array($v['name'])) { - if ($gpc) { - $v['name'] = stripSlashes($v['name']); - } - if ($this->encoding) { - $v['name'] = preg_replace(self::NONCHARS, '', Strings::fixEncoding($v['name'])); - } - $v['@'] = new FileUpload($v); - continue; - } - - foreach ($v['name'] as $k => $foo) { - if ($this->encoding && is_string($k) && (preg_match(self::NONCHARS, $k) || preg_last_error())) { - continue; - } - $list[] = array( - 'name' => $v['name'][$k], - 'type' => $v['type'][$k], - 'size' => $v['size'][$k], - 'tmp_name' => $v['tmp_name'][$k], - 'error' => $v['error'][$k], - '@' => & $v['@'][$k], - ); - } - } - - error_reporting($old); - - - // HEADERS - if (function_exists('apache_request_headers')) { - $headers = array_change_key_case(apache_request_headers(), CASE_LOWER); - } else { - $headers = array(); - foreach ($_SERVER as $k => $v) { - if (strncmp($k, 'HTTP_', 5) == 0) { - $k = substr($k, 5); - } elseif (strncmp($k, 'CONTENT_', 8)) { - continue; - } - $headers[ strtr(strtolower($k), '_', '-') ] = $v; - } - } - - return new Request($url, $query, $post, $files, $cookies, $headers, - isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : NULL, - isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : NULL, - isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : NULL - ); - } - -} diff --git a/apigen/libs/Nette/Nette/Http/Response.php b/apigen/libs/Nette/Nette/Http/Response.php deleted file mode 100644 index 0a63d2a67f9..00000000000 --- a/apigen/libs/Nette/Nette/Http/Response.php +++ /dev/null @@ -1,339 +0,0 @@ -1, 201=>1, 202=>1, 203=>1, 204=>1, 205=>1, 206=>1, - 300=>1, 301=>1, 302=>1, 303=>1, 304=>1, 307=>1, - 400=>1, 401=>1, 403=>1, 404=>1, 405=>1, 406=>1, 408=>1, 410=>1, 412=>1, 415=>1, 416=>1, - 500=>1, 501=>1, 503=>1, 505=>1 - ); - - if (!isset($allowed[$code])) { - throw new Nette\InvalidArgumentException("Bad HTTP response '$code'."); - - } elseif (headers_sent($file, $line)) { - throw new Nette\InvalidStateException("Cannot set HTTP code after HTTP headers have been sent" . ($file ? " (output started at $file:$line)." : ".")); - - } else { - $this->code = $code; - $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; - header($protocol . ' ' . $code, TRUE, $code); - } - return $this; - } - - - - /** - * Returns HTTP response code. - * @return int - */ - public function getCode() - { - return $this->code; - } - - - - /** - * Sends a HTTP header and replaces a previous one. - * @param string header name - * @param string header value - * @return Response provides a fluent interface - * @throws Nette\InvalidStateException if HTTP headers have been sent - */ - public function setHeader($name, $value) - { - if (headers_sent($file, $line)) { - throw new Nette\InvalidStateException("Cannot send header after HTTP headers have been sent" . ($file ? " (output started at $file:$line)." : ".")); - } - - if ($value === NULL && function_exists('header_remove')) { - header_remove($name); - } else { - header($name . ': ' . $value, TRUE, $this->code); - } - return $this; - } - - - - /** - * Adds HTTP header. - * @param string header name - * @param string header value - * @return Response provides a fluent interface - * @throws Nette\InvalidStateException if HTTP headers have been sent - */ - public function addHeader($name, $value) - { - if (headers_sent($file, $line)) { - throw new Nette\InvalidStateException("Cannot send header after HTTP headers have been sent" . ($file ? " (output started at $file:$line)." : ".")); - } - - header($name . ': ' . $value, FALSE, $this->code); - return $this; - } - - - - /** - * Sends a Content-type HTTP header. - * @param string mime-type - * @param string charset - * @return Response provides a fluent interface - * @throws Nette\InvalidStateException if HTTP headers have been sent - */ - public function setContentType($type, $charset = NULL) - { - $this->setHeader('Content-Type', $type . ($charset ? '; charset=' . $charset : '')); - return $this; - } - - - - /** - * Redirects to a new URL. Note: call exit() after it. - * @param string URL - * @param int HTTP code - * @return void - * @throws Nette\InvalidStateException if HTTP headers have been sent - */ - public function redirect($url, $code = self::S302_FOUND) - { - if (isset($_SERVER['SERVER_SOFTWARE']) && preg_match('#^Microsoft-IIS/[1-5]#', $_SERVER['SERVER_SOFTWARE']) - && $this->getHeader('Set-Cookie') !== NULL - ) { - $this->setHeader('Refresh', "0;url=$url"); - return; - } - - $this->setCode($code); - $this->setHeader('Location', $url); - echo "

Redirect

\n\n

Please click here to continue.

"; - } - - - - /** - * Sets the number of seconds before a page cached on a browser expires. - * @param string|int|DateTime time, value 0 means "until the browser is closed" - * @return Response provides a fluent interface - * @throws Nette\InvalidStateException if HTTP headers have been sent - */ - public function setExpiration($time) - { - if (!$time) { // no cache - $this->setHeader('Cache-Control', 's-maxage=0, max-age=0, must-revalidate'); - $this->setHeader('Expires', 'Mon, 23 Jan 1978 10:00:00 GMT'); - return $this; - } - - $time = Nette\DateTime::from($time); - $this->setHeader('Cache-Control', 'max-age=' . ($time->format('U') - time())); - $this->setHeader('Expires', self::date($time)); - return $this; - } - - - - /** - * Checks if headers have been sent. - * @return bool - */ - public function isSent() - { - return headers_sent(); - } - - - - /** - * Return the value of the HTTP header. - * @param string - * @param mixed - * @return mixed - */ - public function getHeader($header, $default = NULL) - { - $header .= ':'; - $len = strlen($header); - foreach (headers_list() as $item) { - if (strncasecmp($item, $header, $len) === 0) { - return ltrim(substr($item, $len)); - } - } - return $default; - } - - - - /** - * Returns a list of headers to sent. - * @return array - */ - public function getHeaders() - { - $headers = array(); - foreach (headers_list() as $header) { - $a = strpos($header, ':'); - $headers[substr($header, 0, $a)] = (string) substr($header, $a + 2); - } - return $headers; - } - - - - /** - * Returns HTTP valid date format. - * @param string|int|DateTime - * @return string - */ - public static function date($time = NULL) - { - $time = Nette\DateTime::from($time); - $time->setTimezone(new \DateTimeZone('GMT')); - return $time->format('D, d M Y H:i:s \G\M\T'); - } - - - - /** - * @return void - */ - public function __destruct() - { - if (self::$fixIE && isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== FALSE - && in_array($this->code, array(400, 403, 404, 405, 406, 408, 409, 410, 500, 501, 505), TRUE) - && $this->getHeader('Content-Type', 'text/html') === 'text/html' - ) { - echo Nette\Utils\Strings::random(2e3, " \t\r\n"); // sends invisible garbage for IE - self::$fixIE = FALSE; - } - } - - - - /** - * Sends a cookie. - * @param string name of the cookie - * @param string value - * @param string|int|DateTime expiration time, value 0 means "until the browser is closed" - * @param string - * @param string - * @param bool - * @param bool - * @return Response provides a fluent interface - * @throws Nette\InvalidStateException if HTTP headers have been sent - */ - public function setCookie($name, $value, $time, $path = NULL, $domain = NULL, $secure = NULL, $httpOnly = NULL) - { - if (headers_sent($file, $line)) { - throw new Nette\InvalidStateException("Cannot set cookie after HTTP headers have been sent" . ($file ? " (output started at $file:$line)." : ".")); - } - - setcookie( - $name, - $value, - $time ? Nette\DateTime::from($time)->format('U') : 0, - $path === NULL ? $this->cookiePath : (string) $path, - $domain === NULL ? $this->cookieDomain : (string) $domain, - $secure === NULL ? $this->cookieSecure : (bool) $secure, - $httpOnly === NULL ? $this->cookieHttpOnly : (bool) $httpOnly - ); - - if (ini_get('suhosin.cookie.encrypt')) { - return $this; - } - - $flatten = array(); - foreach (headers_list() as $header) { - if (preg_match('#^Set-Cookie: .+?=#', $header, $m)) { - $flatten[$m[0]] = $header; - if (PHP_VERSION_ID < 50300) { // multiple deleting due PHP bug #61605 - header('Set-Cookie:'); - } else { - header_remove('Set-Cookie'); - } - } - } - foreach (array_values($flatten) as $key => $header) { - header($header, $key === 0); - } - - return $this; - } - - - - /** - * Deletes a cookie. - * @param string name of the cookie. - * @param string - * @param string - * @param bool - * @return void - * @throws Nette\InvalidStateException if HTTP headers have been sent - */ - public function deleteCookie($name, $path = NULL, $domain = NULL, $secure = NULL) - { - $this->setCookie($name, FALSE, 0, $path, $domain, $secure); - } - -} diff --git a/apigen/libs/Nette/Nette/Http/Session.php b/apigen/libs/Nette/Nette/Http/Session.php deleted file mode 100644 index c7d17b3bcef..00000000000 --- a/apigen/libs/Nette/Nette/Http/Session.php +++ /dev/null @@ -1,591 +0,0 @@ - '', // must be disabled because PHP implementation is invalid - 'use_cookies' => 1, // must be enabled to prevent Session Hijacking and Fixation - 'use_only_cookies' => 1, // must be enabled to prevent Session Fixation - 'use_trans_sid' => 0, // must be disabled to prevent Session Hijacking and Fixation - - // cookies - 'cookie_lifetime' => 0, // until the browser is closed - 'cookie_path' => '/', // cookie is available within the entire domain - 'cookie_domain' => '', // cookie is available on current subdomain only - 'cookie_secure' => FALSE, // cookie is available on HTTP & HTTPS - 'cookie_httponly' => TRUE,// must be enabled to prevent Session Hijacking - - // other - 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,// 3 hours - 'cache_limiter' => NULL, // (default "nocache", special value "\0") - 'cache_expire' => NULL, // (default "180") - 'hash_function' => NULL, // (default "0", means MD5) - 'hash_bits_per_character' => NULL, // (default "4") - ); - - /** @var IRequest */ - private $request; - - /** @var IResponse */ - private $response; - - - - public function __construct(IRequest $request, IResponse $response) - { - $this->request = $request; - $this->response = $response; - } - - - - /** - * Starts and initializes session data. - * @throws Nette\InvalidStateException - * @return void - */ - public function start() - { - if (self::$started) { - return; - } - - $this->configure($this->options); - - Nette\Diagnostics\Debugger::tryError(); - session_start(); - if (Nette\Diagnostics\Debugger::catchError($e) && !session_id()) { - @session_write_close(); // this is needed - throw new Nette\InvalidStateException('session_start(): ' . $e->getMessage(), 0, $e); - } - - self::$started = TRUE; - - /* structure: - __NF: Counter, BrowserKey, Data, Meta, Time - DATA: section->variable = data - META: section->variable = Timestamp, Browser, Version - */ - - unset($_SESSION['__NT'], $_SESSION['__NS'], $_SESSION['__NM']); // old unused structures - - // initialize structures - $nf = & $_SESSION['__NF']; - if (empty($nf)) { // new session - $nf = array('C' => 0); - } else { - $nf['C']++; - } - - // session regenerate every 30 minutes - $nfTime = & $nf['Time']; - $time = time(); - if ($time - $nfTime > self::REGENERATE_INTERVAL) { - $this->regenerated = $this->regenerated || isset($nfTime); - $nfTime = $time; - } - - // browser closing detection - $browserKey = $this->request->getCookie('nette-browser'); - if (!$browserKey) { - $browserKey = Nette\Utils\Strings::random(); - } - $browserClosed = !isset($nf['B']) || $nf['B'] !== $browserKey; - $nf['B'] = $browserKey; - - // resend cookie - $this->sendCookie(); - - // process meta metadata - if (isset($nf['META'])) { - $now = time(); - // expire section variables - foreach ($nf['META'] as $section => $metadata) { - if (is_array($metadata)) { - foreach ($metadata as $variable => $value) { - if ((!empty($value['B']) && $browserClosed) || (!empty($value['T']) && $now > $value['T']) // whenBrowserIsClosed || Time - || (isset($nf['DATA'][$section][$variable]) && is_object($nf['DATA'][$section][$variable]) && (isset($value['V']) ? $value['V'] : NULL) // Version - != Nette\Reflection\ClassType::from($nf['DATA'][$section][$variable])->getAnnotation('serializationVersion')) // intentionally != - ) { - if ($variable === '') { // expire whole section - unset($nf['META'][$section], $nf['DATA'][$section]); - continue 2; - } - unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]); - } - } - } - } - } - - if ($this->regenerated) { - $this->regenerated = FALSE; - $this->regenerateId(); - } - - register_shutdown_function(array($this, 'clean')); - } - - - - /** - * Has been session started? - * @return bool - */ - public function isStarted() - { - return (bool) self::$started; - } - - - - /** - * Ends the current session and store session data. - * @return void - */ - public function close() - { - if (self::$started) { - $this->clean(); - session_write_close(); - self::$started = FALSE; - } - } - - - - /** - * Destroys all data registered to a session. - * @return void - */ - public function destroy() - { - if (!self::$started) { - throw new Nette\InvalidStateException('Session is not started.'); - } - - session_destroy(); - $_SESSION = NULL; - self::$started = FALSE; - if (!$this->response->isSent()) { - $params = session_get_cookie_params(); - $this->response->deleteCookie(session_name(), $params['path'], $params['domain'], $params['secure']); - } - } - - - - /** - * Does session exists for the current request? - * @return bool - */ - public function exists() - { - return self::$started || $this->request->getCookie($this->getName()) !== NULL; - } - - - - /** - * Regenerates the session ID. - * @throws Nette\InvalidStateException - * @return void - */ - public function regenerateId() - { - if (self::$started && !$this->regenerated) { - if (headers_sent($file, $line)) { - throw new Nette\InvalidStateException("Cannot regenerate session ID after HTTP headers have been sent" . ($file ? " (output started at $file:$line)." : ".")); - } - session_regenerate_id(TRUE); - session_write_close(); - $backup = $_SESSION; - session_start(); - $_SESSION = $backup; - } - $this->regenerated = TRUE; - } - - - - /** - * Returns the current session ID. Don't make dependencies, can be changed for each request. - * @return string - */ - public function getId() - { - return session_id(); - } - - - - /** - * Sets the session name to a specified one. - * @param string - * @return Session provides a fluent interface - */ - public function setName($name) - { - if (!is_string($name) || !preg_match('#[^0-9.][^.]*$#A', $name)) { - throw new Nette\InvalidArgumentException('Session name must be a string and cannot contain dot.'); - } - - session_name($name); - return $this->setOptions(array( - 'name' => $name, - )); - } - - - - /** - * Gets the session name. - * @return string - */ - public function getName() - { - return isset($this->options['name']) ? $this->options['name'] : session_name(); - } - - - - /********************* sections management ****************d*g**/ - - - - /** - * Returns specified session section. - * @param string - * @param string - * @return SessionSection - * @throws Nette\InvalidArgumentException - */ - public function getSection($section, $class = 'Nette\Http\SessionSection') - { - return new $class($this, $section); - } - - - - /** @deprecated */ - function getNamespace($section) - { - trigger_error(__METHOD__ . '() is deprecated; use getSection() instead.', E_USER_WARNING); - return $this->getSection($section); - } - - - - /** - * Checks if a session section exist and is not empty. - * @param string - * @return bool - */ - public function hasSection($section) - { - if ($this->exists() && !self::$started) { - $this->start(); - } - - return !empty($_SESSION['__NF']['DATA'][$section]); - } - - - - /** - * Iteration over all sections. - * @return \ArrayIterator - */ - public function getIterator() - { - if ($this->exists() && !self::$started) { - $this->start(); - } - - if (isset($_SESSION['__NF']['DATA'])) { - return new \ArrayIterator(array_keys($_SESSION['__NF']['DATA'])); - - } else { - return new \ArrayIterator; - } - } - - - - /** - * Cleans and minimizes meta structures. - * @return void - */ - public function clean() - { - if (!self::$started || empty($_SESSION)) { - return; - } - - $nf = & $_SESSION['__NF']; - if (isset($nf['META']) && is_array($nf['META'])) { - foreach ($nf['META'] as $name => $foo) { - if (empty($nf['META'][$name])) { - unset($nf['META'][$name]); - } - } - } - - if (empty($nf['META'])) { - unset($nf['META']); - } - - if (empty($nf['DATA'])) { - unset($nf['DATA']); - } - - if (empty($_SESSION)) { - //$this->destroy(); only when shutting down - } - } - - - - /********************* configuration ****************d*g**/ - - - - /** - * Sets session options. - * @param array - * @return Session provides a fluent interface - * @throws Nette\NotSupportedException - * @throws Nette\InvalidStateException - */ - public function setOptions(array $options) - { - if (self::$started) { - $this->configure($options); - } - $this->options = $options + $this->options; - if (!empty($options['auto_start'])) { - $this->start(); - } - return $this; - } - - - - /** - * Returns all session options. - * @return array - */ - public function getOptions() - { - return $this->options; - } - - - - /** - * Configurates session environment. - * @param array - * @return void - */ - private function configure(array $config) - { - $special = array('cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1); - - foreach ($config as $key => $value) { - if (!strncmp($key, 'session.', 8)) { // back compatibility - $key = substr($key, 8); - } - $key = strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $key)); - - if ($value === NULL || ini_get("session.$key") == $value) { // intentionally == - continue; - - } elseif (strncmp($key, 'cookie_', 7) === 0) { - if (!isset($cookie)) { - $cookie = session_get_cookie_params(); - } - $cookie[substr($key, 7)] = $value; - - } else { - if (defined('SID')) { - throw new Nette\InvalidStateException("Unable to set 'session.$key' to value '$value' when session has been started" . ($this->started ? "." : " by session.auto_start or session_start().")); - } - if (isset($special[$key])) { - $key = "session_$key"; - $key($value); - - } elseif (function_exists('ini_set')) { - ini_set("session.$key", $value); - - } elseif (!Nette\Framework::$iAmUsingBadHost) { - throw new Nette\NotSupportedException('Required function ini_set() is disabled.'); - } - } - } - - if (isset($cookie)) { - session_set_cookie_params( - $cookie['lifetime'], $cookie['path'], $cookie['domain'], - $cookie['secure'], $cookie['httponly'] - ); - if (self::$started) { - $this->sendCookie(); - } - } - } - - - - /** - * Sets the amount of time allowed between requests before the session will be terminated. - * @param string|int|DateTime time, value 0 means "until the browser is closed" - * @return Session provides a fluent interface - */ - public function setExpiration($time) - { - if (empty($time)) { - return $this->setOptions(array( - 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME, - 'cookie_lifetime' => 0, - )); - - } else { - $time = Nette\DateTime::from($time)->format('U') - time(); - return $this->setOptions(array( - 'gc_maxlifetime' => $time, - 'cookie_lifetime' => $time, - )); - } - } - - - - /** - * Sets the session cookie parameters. - * @param string path - * @param string domain - * @param bool secure - * @return Session provides a fluent interface - */ - public function setCookieParameters($path, $domain = NULL, $secure = NULL) - { - return $this->setOptions(array( - 'cookie_path' => $path, - 'cookie_domain' => $domain, - 'cookie_secure' => $secure - )); - } - - - - /** - * Returns the session cookie parameters. - * @return array containing items: lifetime, path, domain, secure, httponly - */ - public function getCookieParameters() - { - return session_get_cookie_params(); - } - - - - /** @deprecated */ - function setCookieParams($path, $domain = NULL, $secure = NULL) - { - trigger_error(__METHOD__ . '() is deprecated; use setCookieParameters() instead.', E_USER_WARNING); - return $this->setCookieParameters($path, $domain, $secure); - } - - - - /** - * Sets path of the directory used to save session data. - * @return Session provides a fluent interface - */ - public function setSavePath($path) - { - return $this->setOptions(array( - 'save_path' => $path, - )); - } - - - - /** - * Sets user session storage. - * @return Session provides a fluent interface - */ - public function setStorage(ISessionStorage $storage) - { - if (self::$started) { - throw new Nette\InvalidStateException("Unable to set storage when session has been started."); - } - session_set_save_handler( - array($storage, 'open'), array($storage, 'close'), array($storage, 'read'), - array($storage, 'write'), array($storage, 'remove'), array($storage, 'clean') - ); - } - - - - /** - * Sends the session cookies. - * @return void - */ - private function sendCookie() - { - $cookie = $this->getCookieParameters(); - $this->response->setCookie( - session_name(), session_id(), - $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0, - $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly'] - - )->setCookie( - 'nette-browser', $_SESSION['__NF']['B'], - Response::BROWSER, $cookie['path'], $cookie['domain'] - ); - } - -} diff --git a/apigen/libs/Nette/Nette/Http/SessionSection.php b/apigen/libs/Nette/Nette/Http/SessionSection.php deleted file mode 100644 index 5fa0e4738de..00000000000 --- a/apigen/libs/Nette/Nette/Http/SessionSection.php +++ /dev/null @@ -1,273 +0,0 @@ -session = $session; - $this->name = $name; - } - - - - /** - * Do not call directly. Use Session::getNamespace(). - */ - private function start() - { - if ($this->meta === FALSE) { - $this->session->start(); - $this->data = & $_SESSION['__NF']['DATA'][$this->name]; - $this->meta = & $_SESSION['__NF']['META'][$this->name]; - } - } - - - - /** - * Returns an iterator over all section variables. - * @return \ArrayIterator - */ - public function getIterator() - { - $this->start(); - if (isset($this->data)) { - return new \ArrayIterator($this->data); - } else { - return new \ArrayIterator; - } - } - - - - /** - * Sets a variable in this session section. - * @param string name - * @param mixed value - * @return void - */ - public function __set($name, $value) - { - $this->start(); - $this->data[$name] = $value; - if (is_object($value)) { - $this->meta[$name]['V'] = Nette\Reflection\ClassType::from($value)->getAnnotation('serializationVersion'); - } - } - - - - /** - * Gets a variable from this session section. - * @param string name - * @return mixed - */ - public function &__get($name) - { - $this->start(); - if ($this->warnOnUndefined && !array_key_exists($name, $this->data)) { - trigger_error("The variable '$name' does not exist in session section", E_USER_NOTICE); - } - - return $this->data[$name]; - } - - - - /** - * Determines whether a variable in this session section is set. - * @param string name - * @return bool - */ - public function __isset($name) - { - if ($this->session->exists()) { - $this->start(); - } - return isset($this->data[$name]); - } - - - - /** - * Unsets a variable in this session section. - * @param string name - * @return void - */ - public function __unset($name) - { - $this->start(); - unset($this->data[$name], $this->meta[$name]); - } - - - - /** - * Sets a variable in this session section. - * @param string name - * @param mixed value - * @return void - */ - public function offsetSet($name, $value) - { - $this->__set($name, $value); - } - - - - /** - * Gets a variable from this session section. - * @param string name - * @return mixed - */ - public function offsetGet($name) - { - return $this->__get($name); - } - - - - /** - * Determines whether a variable in this session section is set. - * @param string name - * @return bool - */ - public function offsetExists($name) - { - return $this->__isset($name); - } - - - - /** - * Unsets a variable in this session section. - * @param string name - * @return void - */ - public function offsetUnset($name) - { - $this->__unset($name); - } - - - - /** - * Sets the expiration of the section or specific variables. - * @param string|int|DateTime time, value 0 means "until the browser is closed" - * @param mixed optional list of variables / single variable to expire - * @return SessionSection provides a fluent interface - */ - public function setExpiration($time, $variables = NULL) - { - $this->start(); - if (empty($time)) { - $time = NULL; - $whenBrowserIsClosed = TRUE; - } else { - $time = Nette\DateTime::from($time)->format('U'); - $max = ini_get('session.gc_maxlifetime'); - if ($time - time() > $max + 3) { // bulgarian constant - trigger_error("The expiration time is greater than the session expiration $max seconds", E_USER_NOTICE); - } - $whenBrowserIsClosed = FALSE; - } - - if ($variables === NULL) { // to entire section - $this->meta['']['T'] = $time; - $this->meta['']['B'] = $whenBrowserIsClosed; - - } elseif (is_array($variables)) { // to variables - foreach ($variables as $variable) { - $this->meta[$variable]['T'] = $time; - $this->meta[$variable]['B'] = $whenBrowserIsClosed; - } - - } else { // to variable - $this->meta[$variables]['T'] = $time; - $this->meta[$variables]['B'] = $whenBrowserIsClosed; - } - return $this; - } - - - - /** - * Removes the expiration from the section or specific variables. - * @param mixed optional list of variables / single variable to expire - * @return void - */ - public function removeExpiration($variables = NULL) - { - $this->start(); - if ($variables === NULL) { - // from entire section - unset($this->meta['']['T'], $this->meta['']['B']); - - } elseif (is_array($variables)) { - // from variables - foreach ($variables as $variable) { - unset($this->meta[$variable]['T'], $this->meta[$variable]['B']); - } - } else { - unset($this->meta[$variables]['T'], $this->meta[$variable]['B']); - } - } - - - - /** - * Cancels the current session section. - * @return void - */ - public function remove() - { - $this->start(); - $this->data = NULL; - $this->meta = NULL; - } - -} diff --git a/apigen/libs/Nette/Nette/Http/Url.php b/apigen/libs/Nette/Nette/Http/Url.php deleted file mode 100644 index 381fcd37d1c..00000000000 --- a/apigen/libs/Nette/Nette/Http/Url.php +++ /dev/null @@ -1,526 +0,0 @@ - - * scheme user password host port basePath relativeUrl - * | | | | | | | - * /--\ /--\ /------\ /-------\ /--\/--\/----------------------------\ - * http://john:x0y17575@nette.org:8042/en/manual.php?name=param#fragment <-- absoluteUrl - * \__________________________/\____________/^\________/^\______/ - * | | | | - * authority path query fragment - * - * - * - authority: [user[:password]@]host[:port] - * - hostUrl: http://user:password@nette.org:8042 - * - basePath: /en/ (everything before relative URI not including the script name) - * - baseUrl: http://user:password@nette.org:8042/en/ - * - relativeUrl: manual.php - * - * @author David Grudl - * - * @property string $scheme - * @property string $user - * @property string $password - * @property string $host - * @property string $port - * @property string $path - * @property string $query - * @property string $fragment - * @property-read string $absoluteUrl - * @property-read string $authority - * @property-read string $hostUrl - * @property-read string $basePath - * @property-read string $baseUrl - * @property-read string $relativeUrl - */ -class Url extends Nette\FreezableObject -{ - /** @var array */ - public static $defaultPorts = array( - 'http' => 80, - 'https' => 443, - 'ftp' => 21, - 'news' => 119, - 'nntp' => 119, - ); - - /** @var string */ - private $scheme = ''; - - /** @var string */ - private $user = ''; - - /** @var string */ - private $pass = ''; - - /** @var string */ - private $host = ''; - - /** @var int */ - private $port = NULL; - - /** @var string */ - private $path = ''; - - /** @var string */ - private $query = ''; - - /** @var string */ - private $fragment = ''; - - - - /** - * @param string URL - * @throws Nette\InvalidArgumentException - */ - public function __construct($url = NULL) - { - if (is_string($url)) { - $parts = @parse_url($url); // @ - is escalated to exception - if ($parts === FALSE) { - throw new Nette\InvalidArgumentException("Malformed or unsupported URI '$url'."); - } - - foreach ($parts as $key => $val) { - $this->$key = $val; - } - - if (!$this->port && isset(self::$defaultPorts[$this->scheme])) { - $this->port = self::$defaultPorts[$this->scheme]; - } - - if ($this->path === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { - $this->path = '/'; - } - - } elseif ($url instanceof self) { - foreach ($this as $key => $val) { - $this->$key = $url->$key; - } - } - } - - - - /** - * Sets the scheme part of URI. - * @param string - * @return Url provides a fluent interface - */ - public function setScheme($value) - { - $this->updating(); - $this->scheme = (string) $value; - return $this; - } - - - - /** - * Returns the scheme part of URI. - * @return string - */ - public function getScheme() - { - return $this->scheme; - } - - - - /** - * Sets the user name part of URI. - * @param string - * @return Url provides a fluent interface - */ - public function setUser($value) - { - $this->updating(); - $this->user = (string) $value; - return $this; - } - - - - /** - * Returns the user name part of URI. - * @return string - */ - public function getUser() - { - return $this->user; - } - - - - /** - * Sets the password part of URI. - * @param string - * @return Url provides a fluent interface - */ - public function setPassword($value) - { - $this->updating(); - $this->pass = (string) $value; - return $this; - } - - - - /** - * Returns the password part of URI. - * @return string - */ - public function getPassword() - { - return $this->pass; - } - - - - /** - * Sets the host part of URI. - * @param string - * @return Url provides a fluent interface - */ - public function setHost($value) - { - $this->updating(); - $this->host = (string) $value; - return $this; - } - - - - /** - * Returns the host part of URI. - * @return string - */ - public function getHost() - { - return $this->host; - } - - - - /** - * Sets the port part of URI. - * @param string - * @return Url provides a fluent interface - */ - public function setPort($value) - { - $this->updating(); - $this->port = (int) $value; - return $this; - } - - - - /** - * Returns the port part of URI. - * @return string - */ - public function getPort() - { - return $this->port; - } - - - - /** - * Sets the path part of URI. - * @param string - * @return Url provides a fluent interface - */ - public function setPath($value) - { - $this->updating(); - $this->path = (string) $value; - return $this; - } - - - - /** - * Returns the path part of URI. - * @return string - */ - public function getPath() - { - return $this->path; - } - - - - /** - * Sets the query part of URI. - * @param string|array - * @return Url provides a fluent interface - */ - public function setQuery($value) - { - $this->updating(); - $this->query = (string) (is_array($value) ? http_build_query($value, '', '&') : $value); - return $this; - } - - - - /** - * Appends the query part of URI. - * @param string|array - * @return void - */ - public function appendQuery($value) - { - $this->updating(); - $value = (string) (is_array($value) ? http_build_query($value, '', '&') : $value); - $this->query .= ($this->query === '' || $value === '') ? $value : '&' . $value; - } - - - - /** - * Returns the query part of URI. - * @return string - */ - public function getQuery() - { - return $this->query; - } - - - - /** - * Sets the fragment part of URI. - * @param string - * @return Url provides a fluent interface - */ - public function setFragment($value) - { - $this->updating(); - $this->fragment = (string) $value; - return $this; - } - - - - /** - * Returns the fragment part of URI. - * @return string - */ - public function getFragment() - { - return $this->fragment; - } - - - - /** - * Returns the entire URI including query string and fragment. - * @return string - */ - public function getAbsoluteUrl() - { - return $this->scheme . '://' . $this->getAuthority() . $this->path - . ($this->query === '' ? '' : '?' . $this->query) - . ($this->fragment === '' ? '' : '#' . $this->fragment); - } - - - - /** - * Returns the [user[:pass]@]host[:port] part of URI. - * @return string - */ - public function getAuthority() - { - $authority = $this->host; - if ($this->port && isset(self::$defaultPorts[$this->scheme]) && $this->port !== self::$defaultPorts[$this->scheme]) { - $authority .= ':' . $this->port; - } - - if ($this->user !== '' && $this->scheme !== 'http' && $this->scheme !== 'https') { - $authority = $this->user . ($this->pass === '' ? '' : ':' . $this->pass) . '@' . $authority; - } - - return $authority; - } - - - - /** - * Returns the scheme and authority part of URI. - * @return string - */ - public function getHostUrl() - { - return $this->scheme . '://' . $this->getAuthority(); - } - - - - /** - * Returns the base-path. - * @return string - */ - public function getBasePath() - { - $pos = strrpos($this->path, '/'); - return $pos === FALSE ? '' : substr($this->path, 0, $pos + 1); - } - - - - /** - * Returns the base-URI. - * @return string - */ - public function getBaseUrl() - { - return $this->scheme . '://' . $this->getAuthority() . $this->getBasePath(); - } - - - - /** - * Returns the relative-URI. - * @return string - */ - public function getRelativeUrl() - { - return (string) substr($this->getAbsoluteUrl(), strlen($this->getBaseUrl())); - } - - - - /** - * URI comparsion (this object must be in canonical form). - * @param string - * @return bool - */ - public function isEqual($url) - { - // compare host + path - $part = self::unescape(strtok($url, '?#'), '%/'); - if (strncmp($part, '//', 2) === 0) { // absolute URI without scheme - if ($part !== '//' . $this->getAuthority() . $this->path) { - return FALSE; - } - - } elseif (strncmp($part, '/', 1) === 0) { // absolute path - if ($part !== $this->path) { - return FALSE; - } - - } else { - if ($part !== $this->scheme . '://' . $this->getAuthority() . $this->path) { - return FALSE; - } - } - - // compare query strings - $part = preg_split('#[&;]#', self::unescape(strtr((string) strtok('?#'), '+', ' '), '%&;=+')); - sort($part); - $query = preg_split('#[&;]#', $this->query); - sort($query); - return $part === $query; - } - - - - /** - * Transform to canonical form. - * @return void - */ - public function canonicalize() - { - $this->updating(); - $this->path = $this->path === '' ? '/' : self::unescape($this->path, '%/'); - $this->host = strtolower(rawurldecode($this->host)); - $this->query = self::unescape(strtr($this->query, '+', ' '), '%&;=+'); - } - - - - /** - * @return string - */ - public function __toString() - { - return $this->getAbsoluteUrl(); - } - - - - /** - * Similar to rawurldecode, but preserve reserved chars encoded. - * @param string to decode - * @param string reserved characters - * @return string - */ - public static function unescape($s, $reserved = '%;/?:@&=+$,') - { - // reserved (@see RFC 2396) = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," - // within a path segment, the characters "/", ";", "=", "?" are reserved - // within a query component, the characters ";", "/", "?", ":", "@", "&", "=", "+", ",", "$" are reserved. - preg_match_all('#(?<=%)[a-f0-9][a-f0-9]#i', $s, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); - foreach (array_reverse($matches) as $match) { - $ch = chr(hexdec($match[0][0])); - if (strpos($reserved, $ch) === FALSE) { - $s = substr_replace($s, $ch, $match[0][1] - 1, 3); - } - } - return $s; - } - - - - /** @deprecated */ - function getRelativeUri() - { - trigger_error(__METHOD__ . '() is deprecated; use ' . __CLASS__ . '::getRelativeUrl() instead.', E_USER_WARNING); - return $this->getRelativeUrl(); - } - - /** @deprecated */ - function getAbsoluteUri() - { - trigger_error(__METHOD__ . '() is deprecated; use ' . __CLASS__ . '::getAbsoluteUrl() instead.', E_USER_WARNING); - return $this->getAbsoluteUrl(); - } - - /** @deprecated */ - function getHostUri() - { - trigger_error(__METHOD__ . '() is deprecated; use ' . __CLASS__ . '::getHostUrl() instead.', E_USER_WARNING); - return $this->getHostUrl(); - } - - /** @deprecated */ - function getBaseUri() - { - trigger_error(__METHOD__ . '() is deprecated; use ' . __CLASS__ . '::getBaseUrl() instead.', E_USER_WARNING); - return $this->getBaseUrl(); - } - -} diff --git a/apigen/libs/Nette/Nette/Http/UrlScript.php b/apigen/libs/Nette/Nette/Http/UrlScript.php deleted file mode 100644 index 2aa641c7634..00000000000 --- a/apigen/libs/Nette/Nette/Http/UrlScript.php +++ /dev/null @@ -1,89 +0,0 @@ - - * http://nette.org/admin/script.php/pathinfo/?name=param#fragment - * \_______________/\________/ - * | | - * scriptPath pathInfo - * - * - * - scriptPath: /admin/script.php (or simply /admin/ when script is directory index) - * - pathInfo: /pathinfo/ (additional path information) - * - * @author David Grudl - * - * @property string $scriptPath - * @property-read string $pathInfo - */ -class UrlScript extends Url -{ - /** @var string */ - private $scriptPath = '/'; - - - - /** - * Sets the script-path part of URI. - * @param string - * @return UrlScript provides a fluent interface - */ - public function setScriptPath($value) - { - $this->updating(); - $this->scriptPath = (string) $value; - return $this; - } - - - - /** - * Returns the script-path part of URI. - * @return string - */ - public function getScriptPath() - { - return $this->scriptPath; - } - - - - /** - * Returns the base-path. - * @return string - */ - public function getBasePath() - { - $pos = strrpos($this->scriptPath, '/'); - return $pos === FALSE ? '' : substr($this->path, 0, $pos + 1); - } - - - - /** - * Returns the additional path information. - * @return string - */ - public function getPathInfo() - { - return (string) substr($this->path, strlen($this->scriptPath)); - } - -} diff --git a/apigen/libs/Nette/Nette/Http/UserStorage.php b/apigen/libs/Nette/Nette/Http/UserStorage.php deleted file mode 100644 index 13c93bf2651..00000000000 --- a/apigen/libs/Nette/Nette/Http/UserStorage.php +++ /dev/null @@ -1,221 +0,0 @@ -sessionHandler = $sessionHandler; - } - - - - /** - * Sets the authenticated status of this user. - * @param bool - * @return UserStorage Provides a fluent interface - */ - public function setAuthenticated($state) - { - $section = $this->getSessionSection(TRUE); - $section->authenticated = (bool) $state; - - // Session Fixation defence - $this->sessionHandler->regenerateId(); - - if ($state) { - $section->reason = NULL; - $section->authTime = time(); // informative value - - } else { - $section->reason = self::MANUAL; - $section->authTime = NULL; - } - return $this; - } - - - - /** - * Is this user authenticated? - * @return bool - */ - public function isAuthenticated() - { - $session = $this->getSessionSection(FALSE); - return $session && $session->authenticated; - } - - - - /** - * Sets the user identity. - * @param IIdentity - * @return UserStorage Provides a fluent interface - */ - public function setIdentity(IIdentity $identity = NULL) - { - $this->getSessionSection(TRUE)->identity = $identity; - return $this; - } - - - - /** - * Returns current user identity, if any. - * @return Nette\Security\IIdentity|NULL - */ - public function getIdentity() - { - $session = $this->getSessionSection(FALSE); - return $session ? $session->identity : NULL; - } - - - - /** - * Changes namespace; allows more users to share a session. - * @param string - * @return UserStorage Provides a fluent interface - */ - public function setNamespace($namespace) - { - if ($this->namespace !== $namespace) { - $this->namespace = (string) $namespace; - $this->sessionSection = NULL; - } - return $this; - } - - - - /** - * Returns current namespace. - * @return string - */ - public function getNamespace() - { - return $this->namespace; - } - - - - /** - * Enables log out after inactivity. - * @param string|int|DateTime Number of seconds or timestamp - * @param int Log out when the browser is closed | Clear the identity from persistent storage? - * @return UserStorage Provides a fluent interface - */ - public function setExpiration($time, $flags = 0) - { - $section = $this->getSessionSection(TRUE); - if ($time) { - $time = Nette\DateTime::from($time)->format('U'); - $section->expireTime = $time; - $section->expireDelta = $time - time(); - - } else { - unset($section->expireTime, $section->expireDelta); - } - - $section->expireIdentity = (bool) ($flags & self::CLEAR_IDENTITY); - $section->expireBrowser = (bool) ($flags & self::BROWSER_CLOSED); - $section->browserCheck = TRUE; - $section->setExpiration(0, 'browserCheck'); - $section->setExpiration($time, 'foo'); // time check - return $this; - } - - - - /** - * Why was user logged out? - * @return int - */ - public function getLogoutReason() - { - $session = $this->getSessionSection(FALSE); - return $session ? $session->reason : NULL; - } - - - - /** - * Returns and initializes $this->sessionSection. - * @return SessionSection - */ - protected function getSessionSection($need) - { - if ($this->sessionSection !== NULL) { - return $this->sessionSection; - } - - if (!$need && !$this->sessionHandler->exists()) { - return NULL; - } - - $this->sessionSection = $section = $this->sessionHandler->getSection('Nette.Http.UserStorage/' . $this->namespace); - - if (!$section->identity instanceof IIdentity || !is_bool($section->authenticated)) { - $section->remove(); - } - - if ($section->authenticated && $section->expireBrowser && !$section->browserCheck) { // check if browser was closed? - $section->reason = self::BROWSER_CLOSED; - $section->authenticated = FALSE; - if ($section->expireIdentity) { - unset($section->identity); - } - } - - if ($section->authenticated && $section->expireDelta > 0) { // check time expiration - if ($section->expireTime < time()) { - $section->reason = self::INACTIVITY; - $section->authenticated = FALSE; - if ($section->expireIdentity) { - unset($section->identity); - } - } - $section->expireTime = time() + $section->expireDelta; // sliding expiration - } - - if (!$section->authenticated) { - unset($section->expireTime, $section->expireDelta, $section->expireIdentity, - $section->expireBrowser, $section->browserCheck, $section->authTime); - } - - return $this->sessionSection; - } - -} diff --git a/apigen/libs/Nette/Nette/Iterators/CachingIterator.php b/apigen/libs/Nette/Nette/Iterators/CachingIterator.php deleted file mode 100644 index d97c8114186..00000000000 --- a/apigen/libs/Nette/Nette/Iterators/CachingIterator.php +++ /dev/null @@ -1,266 +0,0 @@ -getIterator(); - - } elseif (!$iterator instanceof \Iterator) { - $iterator = new \IteratorIterator($iterator); - } - - } else { - throw new Nette\InvalidArgumentException("Invalid argument passed to foreach resp. " . __CLASS__ . "; array or Traversable expected, " . (is_object($iterator) ? get_class($iterator) : gettype($iterator)) ." given."); - } - - parent::__construct($iterator, 0); - } - - - - /** - * Is the current element the first one? - * @param int grid width - * @return bool - */ - public function isFirst($width = NULL) - { - return $this->counter === 1 || ($width && $this->counter !== 0 && (($this->counter - 1) % $width) === 0); - } - - - - /** - * Is the current element the last one? - * @param int grid width - * @return bool - */ - public function isLast($width = NULL) - { - return !$this->hasNext() || ($width && ($this->counter % $width) === 0); - } - - - - /** - * Is the iterator empty? - * @return bool - */ - public function isEmpty() - { - return $this->counter === 0; - } - - - - /** - * Is the counter odd? - * @return bool - */ - public function isOdd() - { - return $this->counter % 2 === 1; - } - - - - /** - * Is the counter even? - * @return bool - */ - public function isEven() - { - return $this->counter % 2 === 0; - } - - - - /** - * Returns the counter. - * @return int - */ - public function getCounter() - { - return $this->counter; - } - - - - /** - * Returns the count of elements. - * @return int - */ - public function count() - { - $inner = $this->getInnerIterator(); - if ($inner instanceof \Countable) { - return $inner->count(); - - } else { - throw new Nette\NotSupportedException('Iterator is not countable.'); - } - } - - - - /** - * Forwards to the next element. - * @return void - */ - public function next() - { - parent::next(); - if (parent::valid()) { - $this->counter++; - } - } - - - - /** - * Rewinds the Iterator. - * @return void - */ - public function rewind() - { - parent::rewind(); - $this->counter = parent::valid() ? 1 : 0; - } - - - - /** - * Returns the next key. - * @return mixed - */ - public function getNextKey() - { - return $this->getInnerIterator()->key(); - } - - - - /** - * Returns the next element. - * @return mixed - */ - public function getNextValue() - { - return $this->getInnerIterator()->current(); - } - - - - /********************* Nette\Object behaviour ****************d*g**/ - - - - /** - * Call to undefined method. - * @param string method name - * @param array arguments - * @return mixed - * @throws Nette\MemberAccessException - */ - public function __call($name, $args) - { - return Nette\ObjectMixin::call($this, $name, $args); - } - - - - /** - * Returns property value. Do not call directly. - * @param string property name - * @return mixed property value - * @throws Nette\MemberAccessException if the property is not defined. - */ - public function &__get($name) - { - return Nette\ObjectMixin::get($this, $name); - } - - - - /** - * Sets value of a property. Do not call directly. - * @param string property name - * @param mixed property value - * @return void - * @throws Nette\MemberAccessException if the property is not defined or is read-only - */ - public function __set($name, $value) - { - return Nette\ObjectMixin::set($this, $name, $value); - } - - - - /** - * Is property defined? - * @param string property name - * @return bool - */ - public function __isset($name) - { - return Nette\ObjectMixin::has($this, $name); - } - - - - /** - * Access to undeclared property. - * @param string property name - * @return void - * @throws Nette\MemberAccessException - */ - public function __unset($name) - { - Nette\ObjectMixin::remove($this, $name); - } - - -} diff --git a/apigen/libs/Nette/Nette/Iterators/Filter.php b/apigen/libs/Nette/Nette/Iterators/Filter.php deleted file mode 100644 index 9470fa64c54..00000000000 --- a/apigen/libs/Nette/Nette/Iterators/Filter.php +++ /dev/null @@ -1,47 +0,0 @@ -callback = new Nette\Callback($callback); - } - - - - public function accept() - { - return $this->callback->invoke($this); - } - -} diff --git a/apigen/libs/Nette/Nette/Iterators/InstanceFilter.php b/apigen/libs/Nette/Nette/Iterators/InstanceFilter.php deleted file mode 100644 index dbbc376db0c..00000000000 --- a/apigen/libs/Nette/Nette/Iterators/InstanceFilter.php +++ /dev/null @@ -1,62 +0,0 @@ -type = $type; - parent::__construct($iterator); - } - - - - /** - * Expose the current element of the inner iterator? - * @return bool - */ - public function accept() - { - return $this->current() instanceof $this->type; - } - - - - /** - * Returns the count of elements. - * @return int - */ - public function count() - { - return iterator_count($this); - } - -} diff --git a/apigen/libs/Nette/Nette/Iterators/Mapper.php b/apigen/libs/Nette/Nette/Iterators/Mapper.php deleted file mode 100644 index 5a891b57275..00000000000 --- a/apigen/libs/Nette/Nette/Iterators/Mapper.php +++ /dev/null @@ -1,47 +0,0 @@ -callback = new Nette\Callback($callback); - } - - - - public function current() - { - return $this->callback->invoke(parent::current(), parent::key()); - } - -} diff --git a/apigen/libs/Nette/Nette/Iterators/RecursiveFilter.php b/apigen/libs/Nette/Nette/Iterators/RecursiveFilter.php deleted file mode 100644 index 6b43908f62d..00000000000 --- a/apigen/libs/Nette/Nette/Iterators/RecursiveFilter.php +++ /dev/null @@ -1,66 +0,0 @@ -callback = $callback === NULL ? NULL : new Nette\Callback($callback); - $this->childrenCallback = $childrenCallback === NULL ? NULL : new Nette\Callback($childrenCallback); - } - - - - public function accept() - { - return $this->callback === NULL || $this->callback->invoke($this); - } - - - - public function hasChildren() - { - return $this->getInnerIterator()->hasChildren() - && ($this->childrenCallback === NULL || $this->childrenCallback->invoke($this)); - } - - - - public function getChildren() - { - return new static($this->getInnerIterator()->getChildren(), $this->callback, $this->childrenCallback); - } - -} diff --git a/apigen/libs/Nette/Nette/Iterators/Recursor.php b/apigen/libs/Nette/Nette/Iterators/Recursor.php deleted file mode 100644 index a300eaea972..00000000000 --- a/apigen/libs/Nette/Nette/Iterators/Recursor.php +++ /dev/null @@ -1,60 +0,0 @@ -current(); - return ($obj instanceof \IteratorAggregate && $obj->getIterator() instanceof \RecursiveIterator) - || $obj instanceof \RecursiveIterator; - } - - - - /** - * The sub-iterator for the current element. - * @return \RecursiveIterator - */ - public function getChildren() - { - $obj = $this->current(); - return $obj instanceof \IteratorAggregate ? $obj->getIterator() : $obj; - } - - - - /** - * Returns the count of elements. - * @return int - */ - public function count() - { - return iterator_count($this); - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/Compiler.php b/apigen/libs/Nette/Nette/Latte/Compiler.php deleted file mode 100644 index 2d72152df8d..00000000000 --- a/apigen/libs/Nette/Nette/Latte/Compiler.php +++ /dev/null @@ -1,530 +0,0 @@ - IMacro[]] */ - private $macros; - - /** @var \SplObjectStorage */ - private $macroHandlers; - - /** @var HtmlNode[] */ - private $htmlNodes = array(); - - /** @var MacroNode[] */ - private $macroNodes = array(); - - /** @var string[] */ - private $attrCodes = array(); - - /** @var string */ - private $contentType; - - /** @var array [context, subcontext] */ - private $context; - - /** @var string */ - private $templateId; - - /** Context-aware escaping content types */ - const CONTENT_HTML = 'html', - CONTENT_XHTML = 'xhtml', - CONTENT_XML = 'xml', - CONTENT_JS = 'js', - CONTENT_CSS = 'css', - CONTENT_ICAL = 'ical', - CONTENT_TEXT = 'text'; - - /** @internal Context-aware escaping HTML contexts */ - const CONTEXT_COMMENT = 'comment', - CONTEXT_SINGLE_QUOTED = "'", - CONTEXT_DOUBLE_QUOTED = '"'; - - - public function __construct() - { - $this->macroHandlers = new \SplObjectStorage; - } - - - - /** - * Adds new macro. - * @param string - * @return Compiler provides a fluent interface - */ - public function addMacro($name, IMacro $macro) - { - $this->macros[$name][] = $macro; - $this->macroHandlers->attach($macro); - return $this; - } - - - - /** - * Compiles tokens to PHP code. - * @param Token[] - * @return string - */ - public function compile(array $tokens) - { - $this->templateId = Strings::random(); - $this->tokens = $tokens; - $output = ''; - $this->output = & $output; - $this->htmlNodes = $this->macroNodes = array(); - $this->setContentType($this->defaultContentType); - - foreach ($this->macroHandlers as $handler) { - $handler->initialize($this); - } - - try { - foreach ($tokens as $this->position => $token) { - if ($token->type === Token::TEXT) { - $this->output .= $token->text; - - } elseif ($token->type === Token::MACRO_TAG) { - $isRightmost = !isset($tokens[$this->position + 1]) - || substr($tokens[$this->position + 1]->text, 0, 1) === "\n"; - $this->writeMacro($token->name, $token->value, $token->modifiers, $isRightmost); - - } elseif ($token->type === Token::HTML_TAG_BEGIN) { - $this->processHtmlTagBegin($token); - - } elseif ($token->type === Token::HTML_TAG_END) { - $this->processHtmlTagEnd($token); - - } elseif ($token->type === Token::HTML_ATTRIBUTE) { - $this->processHtmlAttribute($token); - - } elseif ($token->type === Token::COMMENT) { - $this->processComment($token); - } - } - } catch (CompileException $e) { - $e->sourceLine = $token->line; - throw $e; - } - - - foreach ($this->htmlNodes as $htmlNode) { - if (!empty($htmlNode->macroAttrs)) { - throw new CompileException("Missing end tag name> for macro-attribute " . Parser::N_PREFIX - . implode(' and ' . Parser::N_PREFIX, array_keys($htmlNode->macroAttrs)) . ".", 0, $token->line); - } - } - - $prologs = $epilogs = ''; - foreach ($this->macroHandlers as $handler) { - $res = $handler->finalize(); - $handlerName = get_class($handler); - $prologs .= empty($res[0]) ? '' : ""; - $epilogs = (empty($res[1]) ? '' : "") . $epilogs; - } - $output = ($prologs ? $prologs . "\n" : '') . $output . $epilogs; - - if ($this->macroNodes) { - throw new CompileException("There are unclosed macros.", 0, $token->line); - } - - $output = $this->expandTokens($output); - return $output; - } - - - - /** - * @return Compiler provides a fluent interface - */ - public function setContentType($type) - { - $this->contentType = $type; - $this->context = NULL; - return $this; - } - - - - /** - * @return string - */ - public function getContentType() - { - return $this->contentType; - } - - - - /** - * @return Compiler provides a fluent interface - */ - public function setContext($context, $sub = NULL) - { - $this->context = array($context, $sub); - return $this; - } - - - - /** - * @return array [context, subcontext] - */ - public function getContext() - { - return $this->context; - } - - - - /** - * @return string - */ - public function getTemplateId() - { - return $this->templateId; - } - - - - /** - * Returns current line number. - * @return int - */ - public function getLine() - { - return $this->tokens ? $this->tokens[$this->position]->line : NULL; - } - - - - public function expandTokens($s) - { - return strtr($s, $this->attrCodes); - } - - - - private function processHtmlTagBegin(Token $token) - { - if ($token->closing) { - do { - $htmlNode = array_pop($this->htmlNodes); - if (!$htmlNode) { - $htmlNode = new HtmlNode($token->name); - } - if (strcasecmp($htmlNode->name, $token->name) === 0) { - break; - } - if ($htmlNode->macroAttrs) { - throw new CompileException("Unexpected name>.", 0, $token->line); - } - } while (TRUE); - $this->htmlNodes[] = $htmlNode; - $htmlNode->closing = TRUE; - $htmlNode->offset = strlen($this->output); - $this->setContext(NULL); - - } elseif ($token->text === '') { - $this->output .= $token->text; - $this->setContext(NULL); - return; - } - - $htmlNode = end($this->htmlNodes); - $isEmpty = !$htmlNode->closing && (Strings::contains($token->text, '/') || $htmlNode->isEmpty); - - if ($isEmpty && in_array($this->contentType, array(self::CONTENT_HTML, self::CONTENT_XHTML))) { // auto-correct - $token->text = preg_replace('#^.*>#', $this->contentType === self::CONTENT_XHTML ? ' />' : '>', $token->text); - } - - if (empty($htmlNode->macroAttrs)) { - $this->output .= $token->text; - } else { - $code = substr($this->output, $htmlNode->offset) . $token->text; - $this->output = substr($this->output, 0, $htmlNode->offset); - $this->writeAttrsMacro($code, $htmlNode); - if ($isEmpty) { - $htmlNode->closing = TRUE; - $this->writeAttrsMacro('', $htmlNode); - } - } - - if ($isEmpty) { - $htmlNode->closing = TRUE; - } - - if (!$htmlNode->closing && (strcasecmp($htmlNode->name, 'script') === 0 || strcasecmp($htmlNode->name, 'style') === 0)) { - $this->setContext(strcasecmp($htmlNode->name, 'style') ? self::CONTENT_JS : self::CONTENT_CSS); - } else { - $this->setContext(NULL); - if ($htmlNode->closing) { - array_pop($this->htmlNodes); - } - } - } - - - - private function processHtmlAttribute(Token $token) - { - $htmlNode = end($this->htmlNodes); - if (Strings::startsWith($token->name, Parser::N_PREFIX)) { - $name = substr($token->name, strlen(Parser::N_PREFIX)); - if (isset($htmlNode->macroAttrs[$name])) { - throw new CompileException("Found multiple macro-attributes $token->name.", 0, $token->line); - } - $htmlNode->macroAttrs[$name] = $token->value; - - } else { - $htmlNode->attrs[$token->name] = TRUE; - $this->output .= $token->text; - if ($token->value) { // quoted - $context = NULL; - if (strncasecmp($token->name, 'on', 2) === 0) { - $context = self::CONTENT_JS; - } elseif ($token->name === 'style') { - $context = self::CONTENT_CSS; - } - $this->setContext($token->value, $context); - } - } - } - - - - private function processComment(Token $token) - { - $isLeftmost = trim(substr($this->output, strrpos("\n$this->output", "\n"))) === ''; - if (!$isLeftmost) { - $this->output .= substr($token->text, strlen(rtrim($token->text, "\n"))); - } - } - - - - /********************* macros ****************d*g**/ - - - - /** - * Generates code for {macro ...} to the output. - * @param string - * @param string - * @param string - * @param bool - * @return MacroNode - */ - public function writeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE, HtmlNode $htmlNode = NULL, $prefix = NULL) - { - if ($name[0] === '/') { // closing - $node = end($this->macroNodes); - - if (!$node || ("/$node->name" !== $name && '/' !== $name) || $modifiers - || ($args && $node->args && !Strings::startsWith("$node->args ", "$args ")) - ) { - $name .= $args ? ' ' : ''; - throw new CompileException("Unexpected macro {{$name}{$args}{$modifiers}}" - . ($node ? ", expecting {/$node->name}" . ($args && $node->args ? " or eventually {/$node->name $node->args}" : '') : '')); - } - - array_pop($this->macroNodes); - if (!$node->args) { - $node->setArgs($args); - } - - $isLeftmost = $node->content ? trim(substr($this->output, strrpos("\n$this->output", "\n"))) === '' : FALSE; - - $node->closing = TRUE; - $node->macro->nodeClosed($node); - - $this->output = & $node->saved[0]; - $this->writeCode($node->openingCode, $this->output, $node->saved[1]); - $this->writeCode($node->closingCode, $node->content, $isRightmost, $isLeftmost); - $this->output .= $node->content; - - } else { // opening - $node = $this->expandMacro($name, $args, $modifiers, $htmlNode, $prefix); - if ($node->isEmpty) { - $this->writeCode($node->openingCode, $this->output, $isRightmost); - - } else { - $this->macroNodes[] = $node; - $node->saved = array(& $this->output, $isRightmost); - $this->output = & $node->content; - } - } - return $node; - } - - - - private function writeCode($code, & $output, $isRightmost, $isLeftmost = NULL) - { - if ($isRightmost) { - $leftOfs = strrpos("\n$output", "\n"); - $isLeftmost = $isLeftmost === NULL ? trim(substr($output, $leftOfs)) === '' : $isLeftmost; - if ($isLeftmost && substr($code, 0, 11) !== ' remove indentation - } elseif (substr($code, -2) === '?>') { - $code .= "\n"; // double newline to avoid newline eating by PHP - } - } - $output .= $code; - } - - - - /** - * Generates code for macro to the output. - * @param string - * @return void - */ - public function writeAttrsMacro($code, HtmlNode $htmlNode) - { - $attrs = $htmlNode->macroAttrs; - $left = $right = array(); - $attrCode = ''; - - foreach ($this->macros as $name => $foo) { - $attrName = MacroNode::PREFIX_INNER . "-$name"; - if (isset($attrs[$attrName])) { - if ($htmlNode->closing) { - $left[] = array("/$name", '', MacroNode::PREFIX_INNER); - } else { - array_unshift($right, array($name, $attrs[$attrName], MacroNode::PREFIX_INNER)); - } - unset($attrs[$attrName]); - } - } - - foreach (array_reverse($this->macros) as $name => $foo) { - $attrName = MacroNode::PREFIX_TAG . "-$name"; - if (isset($attrs[$attrName])) { - $left[] = array($name, $attrs[$attrName], MacroNode::PREFIX_TAG); - array_unshift($right, array("/$name", '', MacroNode::PREFIX_TAG)); - unset($attrs[$attrName]); - } - } - - foreach ($this->macros as $name => $foo) { - if (isset($attrs[$name])) { - if ($htmlNode->closing) { - $right[] = array("/$name", '', NULL); - } else { - array_unshift($left, array($name, $attrs[$name], NULL)); - } - unset($attrs[$name]); - } - } - - if ($attrs) { - throw new CompileException("Unknown macro-attribute " . Parser::N_PREFIX - . implode(' and ' . Parser::N_PREFIX, array_keys($attrs))); - } - - if (!$htmlNode->closing) { - $htmlNode->attrCode = & $this->attrCodes[$uniq = ' n:' . Nette\Utils\Strings::random()]; - $code = substr_replace($code, $uniq, strrpos($code, '/>') ?: strrpos($code, '>'), 0); - } - - foreach ($left as $item) { - $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode, $item[2]); - if ($node->closing || $node->isEmpty) { - $htmlNode->attrCode .= $node->attrCode; - if ($node->isEmpty) { - unset($htmlNode->macroAttrs[$node->name]); - } - } - } - - $this->output .= $code; - - foreach ($right as $item) { - $node = $this->writeMacro($item[0], $item[1], NULL, NULL, $htmlNode); - if ($node->closing) { - $htmlNode->attrCode .= $node->attrCode; - } - } - - if ($right && substr($this->output, -2) === '?>') { - $this->output .= "\n"; - } - } - - - - /** - * Expands macro and returns node & code. - * @param string - * @param string - * @param string - * @return MacroNode - */ - public function expandMacro($name, $args, $modifiers = NULL, HtmlNode $htmlNode = NULL, $prefix = NULL) - { - if (empty($this->macros[$name])) { - $cdata = $this->htmlNodes && in_array(strtolower(end($this->htmlNodes)->name), array('script', 'style')); - throw new CompileException("Unknown macro {{$name}}" . ($cdata ? " (in JavaScript or CSS, try to put a space after bracket.)" : '')); - } - foreach (array_reverse($this->macros[$name]) as $macro) { - $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL, $htmlNode, $prefix); - if ($macro->nodeOpened($node) !== FALSE) { - return $node; - } - } - throw new CompileException("Unhandled macro {{$name}}"); - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/Engine.php b/apigen/libs/Nette/Nette/Latte/Engine.php deleted file mode 100644 index 468c9cdfc27..00000000000 --- a/apigen/libs/Nette/Nette/Latte/Engine.php +++ /dev/null @@ -1,77 +0,0 @@ -parser = new Parser; - $this->compiler = new Compiler; - $this->compiler->defaultContentType = Compiler::CONTENT_XHTML; - - Macros\CoreMacros::install($this->compiler); - $this->compiler->addMacro('cache', new Macros\CacheMacro($this->compiler)); - Macros\UIMacros::install($this->compiler); - Macros\FormMacros::install($this->compiler); - } - - - - /** - * Invokes filter. - * @param string - * @return string - */ - public function __invoke($s) - { - return $this->compiler->compile($this->parser->parse($s)); - } - - - - /** - * @return Parser - */ - public function getParser() - { - return $this->parser; - } - - - - /** - * @return Compiler - */ - public function getCompiler() - { - return $this->compiler; - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/HtmlNode.php b/apigen/libs/Nette/Nette/Latte/HtmlNode.php deleted file mode 100644 index b4e5858e4a5..00000000000 --- a/apigen/libs/Nette/Nette/Latte/HtmlNode.php +++ /dev/null @@ -1,53 +0,0 @@ -name = $name; - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/IMacro.php b/apigen/libs/Nette/Nette/Latte/IMacro.php deleted file mode 100644 index 55917b70a31..00000000000 --- a/apigen/libs/Nette/Nette/Latte/IMacro.php +++ /dev/null @@ -1,50 +0,0 @@ -macro = $macro; - $this->name = (string) $name; - $this->modifiers = (string) $modifiers; - $this->parentNode = $parentNode; - $this->htmlNode = $htmlNode; - $this->prefix = $prefix; - $this->tokenizer = new MacroTokenizer($this->args); - $this->data = new \stdClass; - $this->setArgs($args); - } - - - - public function setArgs($args) - { - $this->args = (string) $args; - $this->tokenizer->tokenize($this->args); - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/MacroTokenizer.php b/apigen/libs/Nette/Nette/Latte/MacroTokenizer.php deleted file mode 100644 index d1fb7bf88e9..00000000000 --- a/apigen/libs/Nette/Nette/Latte/MacroTokenizer.php +++ /dev/null @@ -1,69 +0,0 @@ - '\s+', - self::T_COMMENT => '(?s)/\*.*?\*/', - self::T_STRING => Parser::RE_STRING, - self::T_KEYWORD => '(?:true|false|null|and|or|xor|clone|new|instanceof|return|continue|break|[A-Z_][A-Z0-9_]{2,})(?![\w\pL_])', // keyword or const - self::T_CAST => '\((?:expand|string|array|int|integer|float|bool|boolean|object)\)', // type casting - self::T_VARIABLE => '\$[\w\pL_]+', - self::T_NUMBER => '[+-]?[0-9]+(?:\.[0-9]+)?(?:e[0-9]+)?', - self::T_SYMBOL => '[\w\pL_]+(?:-[\w\pL_]+)*', - self::T_CHAR => '::|=>|[^"\']', // =>, any char except quotes - ), 'u'); - $this->ignored = array(self::T_COMMENT, self::T_WHITESPACE); - $this->tokenize($input); - } - - - - /** - * Reads single token (optionally delimited by comma) from string. - * @param string - * @return string - */ - public function fetchWord() - { - $word = $this->fetchUntil(self::T_WHITESPACE, ','); - $this->fetch(','); - $this->fetchAll(self::T_WHITESPACE, self::T_COMMENT); - return $word; - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/Macros/CacheMacro.php b/apigen/libs/Nette/Nette/Latte/Macros/CacheMacro.php deleted file mode 100644 index 9da7142be1d..00000000000 --- a/apigen/libs/Nette/Nette/Latte/Macros/CacheMacro.php +++ /dev/null @@ -1,132 +0,0 @@ -used = FALSE; - } - - - - /** - * Finishes template parsing. - * @return array(prolog, epilog) - */ - public function finalize() - { - if ($this->used) { - return array('Nette\Latte\Macros\CacheMacro::initRuntime($template, $_g);'); - } - } - - - - /** - * New node is found. - * @return bool - */ - public function nodeOpened(Latte\MacroNode $node) - { - $this->used = TRUE; - $node->isEmpty = FALSE; - $node->openingCode = Latte\PhpWriter::using($node) - ->write('caches, %node.array?)) { ?>', - Nette\Utils\Strings::random() - ); - } - - - - /** - * Node is closed. - * @return void - */ - public function nodeClosed(Latte\MacroNode $node) - { - $node->closingCode = 'tmp = array_pop($_g->caches); if (!$_l->tmp instanceof stdClass) $_l->tmp->end(); } ?>'; - } - - - - /********************* run-time helpers ****************d*g**/ - - - - /** - * @return void - */ - public static function initRuntime(Nette\Templating\FileTemplate $template, \stdClass $global) - { - if (!empty($global->caches)) { - end($global->caches)->dependencies[Nette\Caching\Cache::FILES][] = $template->getFile(); - } - } - - - - /** - * Starts the output cache. Returns Nette\Caching\OutputHelper object if buffering was started. - * @param Nette\Caching\IStorage - * @param string - * @param Nette\Caching\OutputHelper[] - * @param array - * @return Nette\Caching\OutputHelper - */ - public static function createCache(Nette\Caching\IStorage $cacheStorage, $key, & $parents, array $args = NULL) - { - if ($args) { - if (array_key_exists('if', $args) && !$args['if']) { - return $parents[] = (object) NULL; - } - $key = array_merge(array($key), array_intersect_key($args, range(0, count($args)))); - } - if ($parents) { - end($parents)->dependencies[Nette\Caching\Cache::ITEMS][] = $key; - } - - $cache = new Nette\Caching\Cache($cacheStorage, 'Nette.Templating.Cache'); - if ($helper = $cache->start($key)) { - if (isset($args['expire'])) { - $args['expiration'] = $args['expire']; // back compatibility - } - $helper->dependencies = array( - Nette\Caching\Cache::TAGS => isset($args['tags']) ? $args['tags'] : NULL, - Nette\Caching\Cache::EXPIRATION => isset($args['expiration']) ? $args['expiration'] : '+ 7 days', - ); - $parents[] = $helper; - } - return $helper; - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/Macros/CoreMacros.php b/apigen/libs/Nette/Nette/Latte/Macros/CoreMacros.php deleted file mode 100644 index 35ce3fc1639..00000000000 --- a/apigen/libs/Nette/Nette/Latte/Macros/CoreMacros.php +++ /dev/null @@ -1,415 +0,0 @@ - value} set template parameter - * - {default var => value} set default template parameter - * - {dump $var} - * - {debugbreak} - * - {l} {r} to display { } - * - * @author David Grudl - */ -class CoreMacros extends MacroSet -{ - - - public static function install(Latte\Compiler $compiler) - { - $me = new static($compiler); - - $me->addMacro('if', array($me, 'macroIf'), array($me, 'macroEndIf')); - $me->addMacro('elseif', 'elseif (%node.args):'); - $me->addMacro('else', array($me, 'macroElse')); - $me->addMacro('ifset', 'if (isset(%node.args)):', 'endif'); - $me->addMacro('elseifset', 'elseif (isset(%node.args)):'); - - $me->addMacro('foreach', '', array($me, 'macroEndForeach')); - $me->addMacro('for', 'for (%node.args):', 'endfor'); - $me->addMacro('while', 'while (%node.args):', 'endwhile'); - $me->addMacro('continueIf', 'if (%node.args) continue'); - $me->addMacro('breakIf', 'if (%node.args) break'); - $me->addMacro('first', 'if ($iterator->isFirst(%node.args)):', 'endif'); - $me->addMacro('last', 'if ($iterator->isLast(%node.args)):', 'endif'); - $me->addMacro('sep', 'if (!$iterator->isLast(%node.args)):', 'endif'); - - $me->addMacro('var', array($me, 'macroVar')); - $me->addMacro('assign', array($me, 'macroVar')); // deprecated - $me->addMacro('default', array($me, 'macroVar')); - $me->addMacro('dump', array($me, 'macroDump')); - $me->addMacro('debugbreak', array($me, 'macroDebugbreak')); - $me->addMacro('l', '?>{addMacro('r', '?>}addMacro('_', array($me, 'macroTranslate'), array($me, 'macroTranslate')); - $me->addMacro('=', array($me, 'macroExpr')); - $me->addMacro('?', array($me, 'macroExpr')); - - $me->addMacro('capture', array($me, 'macroCapture'), array($me, 'macroCaptureEnd')); - $me->addMacro('include', array($me, 'macroInclude')); - $me->addMacro('use', array($me, 'macroUse')); - - $me->addMacro('class', NULL, NULL, array($me, 'macroClass')); - $me->addMacro('attr', array($me, 'macroOldAttr'), '', array($me, 'macroAttr')); - $me->addMacro('href', NULL); // TODO: placeholder - } - - - - /** - * Finishes template parsing. - * @return array(prolog, epilog) - */ - public function finalize() - { - return array('list($_l, $_g) = Nette\Latte\Macros\CoreMacros::initRuntime($template, ' - . var_export($this->getCompiler()->getTemplateId(), TRUE) . ')'); - } - - - - /********************* macros ****************d*g**/ - - - - /** - * {if ...} - */ - public function macroIf(MacroNode $node, PhpWriter $writer) - { - if ($node->data->capture = ($node->args === '')) { - return 'ob_start()'; - } - if ($node->prefix === $node::PREFIX_TAG) { - return $writer->write($node->htmlNode->closing ? 'if (array_pop($_l->ifs)):' : 'if ($_l->ifs[] = (%node.args)):'); - } - return $writer->write('if (%node.args):'); - } - - - - /** - * {/if ...} - */ - public function macroEndIf(MacroNode $node, PhpWriter $writer) - { - if ($node->data->capture) { - if ($node->args === '') { - throw new CompileException('Missing condition in {if} macro.'); - } - return $writer->write('if (%node.args) ' - . (isset($node->data->else) ? '{ ob_end_clean(); ob_end_flush(); }' : 'ob_end_flush();') - . ' else ' - . (isset($node->data->else) ? '{ $_else = ob_get_contents(); ob_end_clean(); ob_end_clean(); echo $_else; }' : 'ob_end_clean();') - ); - } - return 'endif'; - } - - - - /** - * {else} - */ - public function macroElse(MacroNode $node, PhpWriter $writer) - { - $ifNode = $node->parentNode; - if ($ifNode && $ifNode->name === 'if' && $ifNode->data->capture) { - if (isset($ifNode->data->else)) { - throw new CompileException("Macro {if} supports only one {else}."); - } - $ifNode->data->else = TRUE; - return 'ob_start()'; - } - return 'else:'; - } - - - - /** - * {_$var |modifiers} - */ - public function macroTranslate(MacroNode $node, PhpWriter $writer) - { - if ($node->closing) { - return $writer->write('echo %modify($template->translate(ob_get_clean()))'); - - } elseif ($node->isEmpty = ($node->args !== '')) { - return $writer->write('echo %modify($template->translate(%node.args))'); - - } else { - return 'ob_start()'; - } - } - - - - /** - * {include "file" [,] [params]} - */ - public function macroInclude(MacroNode $node, PhpWriter $writer) - { - $code = $writer->write('Nette\Latte\Macros\CoreMacros::includeTemplate(%node.word, %node.array? + $template->getParameters(), $_l->templates[%var])', - $this->getCompiler()->getTemplateId()); - - if ($node->modifiers) { - return $writer->write('echo %modify(%raw->__toString(TRUE))', $code); - } else { - return $code . '->render()'; - } - } - - - - /** - * {use class MacroSet} - */ - public function macroUse(MacroNode $node, PhpWriter $writer) - { - Nette\Callback::create($node->tokenizer->fetchWord(), 'install') - ->invoke($this->getCompiler()) - ->initialize(); - } - - - - /** - * {capture $variable} - */ - public function macroCapture(MacroNode $node, PhpWriter $writer) - { - $variable = $node->tokenizer->fetchWord(); - if (substr($variable, 0, 1) !== '$') { - throw new CompileException("Invalid capture block variable '$variable'"); - } - $node->data->variable = $variable; - return 'ob_start()'; - } - - - - /** - * {/capture} - */ - public function macroCaptureEnd(MacroNode $node, PhpWriter $writer) - { - return $node->data->variable . $writer->write(" = %modify(ob_get_clean())"); - } - - - - /** - * {foreach ...} - */ - public function macroEndForeach(MacroNode $node, PhpWriter $writer) - { - if (preg_match('#\W(\$iterator|include|require|get_defined_vars)\W#', $this->getCompiler()->expandTokens($node->content))) { - $node->openingCode = 'its[] = new Nette\Iterators\CachingIterator(' - . preg_replace('#(.*)\s+as\s+#i', '$1) as ', $writer->formatArgs(), 1) . '): ?>'; - $node->closingCode = 'its); $iterator = end($_l->its) ?>'; - } else { - $node->openingCode = 'formatArgs() . '): ?>'; - $node->closingCode = ''; - } - } - - - - /** - * n:class="..." - */ - public function macroClass(MacroNode $node, PhpWriter $writer) - { - return $writer->write('if ($_l->tmp = array_filter(%node.array)) echo \' class="\' . %escape(implode(" ", array_unique($_l->tmp))) . \'"\''); - } - - - - /** - * n:attr="..." - */ - public function macroAttr(MacroNode $node, PhpWriter $writer) - { - return $writer->write('echo Nette\Utils\Html::el(NULL, %node.array)->attributes()'); - } - - - - /** - * {attr ...} - * @deprecated - */ - public function macroOldAttr(MacroNode $node) - { - return Nette\Utils\Strings::replace($node->args . ' ', '#\)\s+#', ')->'); - } - - - - /** - * {dump ...} - */ - public function macroDump(MacroNode $node, PhpWriter $writer) - { - $args = $writer->formatArgs(); - return 'Nette\Diagnostics\Debugger::barDump(' . ($node->args ? "array(" . $writer->write('%var', $args) . " => $args)" : 'get_defined_vars()') - . ', "Template " . str_replace(dirname(dirname($template->getFile())), "\xE2\x80\xA6", $template->getFile()))'; - } - - - - /** - * {debugbreak ...} - */ - public function macroDebugbreak(MacroNode $node, PhpWriter $writer) - { - return $writer->write(($node->args == NULL ? '' : 'if (!(%node.args)); else') - . 'if (function_exists("debugbreak")) debugbreak(); elseif (function_exists("xdebug_break")) xdebug_break()'); - } - - - - /** - * {var ...} - * {default ...} - */ - public function macroVar(MacroNode $node, PhpWriter $writer) - { - $out = ''; - $var = TRUE; - $tokenizer = $writer->preprocess(); - while ($token = $tokenizer->fetchToken()) { - if ($var && ($token['type'] === Latte\MacroTokenizer::T_SYMBOL || $token['type'] === Latte\MacroTokenizer::T_VARIABLE)) { - if ($node->name === 'default') { - $out .= "'" . ltrim($token['value'], "$") . "'"; - } else { - $out .= '$' . ltrim($token['value'], "$"); - } - $var = NULL; - - } elseif (($token['value'] === '=' || $token['value'] === '=>') && $token['depth'] === 0) { - $out .= $node->name === 'default' ? '=>' : '='; - $var = FALSE; - - } elseif ($token['value'] === ',' && $token['depth'] === 0) { - $out .= $node->name === 'default' ? ',' : ';'; - $var = TRUE; - - } elseif ($var === NULL && $node->name === 'default' && $token['type'] !== Latte\MacroTokenizer::T_WHITESPACE) { - throw new CompileException("Unexpected '$token[value]' in {default $node->args}"); - - } else { - $out .= $writer->canQuote($tokenizer) ? "'$token[value]'" : $token['value']; - } - } - return $node->name === 'default' ? "extract(array($out), EXTR_SKIP)" : $out; - } - - - - /** - * {= ...} - * {? ...} - */ - public function macroExpr(MacroNode $node, PhpWriter $writer) - { - return $writer->write(($node->name === '?' ? '' : 'echo ') . '%modify(%node.args)'); - } - - - - /********************* run-time helpers ****************d*g**/ - - - - /** - * Includes subtemplate. - * @param mixed included file name or template - * @param array parameters - * @param Nette\Templating\ITemplate current template - * @return Nette\Templating\Template - */ - public static function includeTemplate($destination, array $params, Nette\Templating\ITemplate $template) - { - if ($destination instanceof Nette\Templating\ITemplate) { - $tpl = $destination; - - } elseif ($destination == NULL) { // intentionally == - throw new Nette\InvalidArgumentException("Template file name was not specified."); - - } elseif ($template instanceof Nette\Templating\IFileTemplate) { - if (substr($destination, 0, 1) !== '/' && substr($destination, 1, 1) !== ':') { - $destination = dirname($template->getFile()) . '/' . $destination; - } - $tpl = clone $template; - $tpl->setFile($destination); - - } else { - throw new Nette\NotSupportedException('Macro {include "filename"} is supported only with Nette\Templating\IFileTemplate.'); - } - - $tpl->setParameters($params); // interface? - return $tpl; - } - - - - /** - * Initializes local & global storage in template. - * @param - * @param string - * @return \stdClass - */ - public static function initRuntime(Nette\Templating\ITemplate $template, $templateId) - { - // local storage - if (isset($template->_l)) { - $local = $template->_l; - unset($template->_l); - } else { - $local = (object) NULL; - } - $local->templates[$templateId] = $template; - - // global storage - if (!isset($template->_g)) { - $template->_g = (object) NULL; - } - - return array($local, $template->_g); - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/Macros/FormMacros.php b/apigen/libs/Nette/Nette/Latte/Macros/FormMacros.php deleted file mode 100644 index c2d26c4c2bf..00000000000 --- a/apigen/libs/Nette/Nette/Latte/Macros/FormMacros.php +++ /dev/null @@ -1,137 +0,0 @@ -addMacro('form', - 'Nette\Latte\Macros\FormMacros::renderFormBegin($form = $_form = (is_object(%node.word) ? %node.word : $_control[%node.word]), %node.array)', - 'Nette\Latte\Macros\FormMacros::renderFormEnd($_form)'); - $me->addMacro('label', array($me, 'macroLabel'), '?>addMacro('input', 'echo $_form[%node.word]->getControl()->addAttributes(%node.array)', NULL, array($me, 'macroAttrInput')); - $me->addMacro('formContainer', '$_formStack[] = $_form; $formContainer = $_form = $_form[%node.word]', '$_form = array_pop($_formStack)'); - } - - - - /********************* macros ****************d*g**/ - - - /** - * {label ...} and optionally {/label} - */ - public function macroLabel(MacroNode $node, PhpWriter $writer) - { - $cmd = 'if ($_label = $_form[%node.word]->getLabel()) echo $_label->addAttributes(%node.array)'; - if ($node->isEmpty = (substr($node->args, -1) === '/')) { - $node->setArgs(substr($node->args, 0, -1)); - return $writer->write($cmd); - } else { - return $writer->write($cmd . '->startTag()'); - } - } - - - - /** - * n:input - */ - public function macroAttrInput(MacroNode $node, PhpWriter $writer) - { - if ($node->htmlNode->attrs) { - $reset = array_fill_keys(array_keys($node->htmlNode->attrs), NULL); - return $writer->write('echo $_form[%node.word]->getControl()->addAttributes(%var)->attributes()', $reset); - } - return $writer->write('echo $_form[%node.word]->getControl()->attributes()'); - } - - - - /********************* run-time writers ****************d*g**/ - - - - /** - * Renders form begin. - * @return void - */ - public static function renderFormBegin(Form $form, array $attrs) - { - $el = $form->getElementPrototype(); - $el->action = (string) $el->action; - $el = clone $el; - if (strcasecmp($form->getMethod(), 'get') === 0) { - list($el->action) = explode('?', $el->action, 2); - } - echo $el->addAttributes($attrs)->startTag(); - } - - - - /** - * Renders form end. - * @return string - */ - public static function renderFormEnd(Form $form) - { - $s = ''; - if (strcasecmp($form->getMethod(), 'get') === 0) { - $url = explode('?', $form->getElementPrototype()->action, 2); - if (isset($url[1])) { - foreach (preg_split('#[;&]#', $url[1]) as $param) { - $parts = explode('=', $param, 2); - $name = urldecode($parts[0]); - if (!isset($form[$name])) { - $s .= Nette\Utils\Html::el('input', array('type' => 'hidden', 'name' => $name, 'value' => urldecode($parts[1]))); - } - } - } - } - - foreach ($form->getComponents(TRUE, 'Nette\Forms\Controls\HiddenField') as $control) { - if (!$control->getOption('rendered')) { - $s .= $control->getControl(); - } - } - - if (iterator_count($form->getComponents(TRUE, 'Nette\Forms\Controls\TextInput')) < 2) { - $s .= ''; - } - - echo ($s ? "
$s
\n" : '') . $form->getElementPrototype()->endTag() . "\n"; - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/Macros/MacroSet.php b/apigen/libs/Nette/Nette/Latte/Macros/MacroSet.php deleted file mode 100644 index 7b1d089f589..00000000000 --- a/apigen/libs/Nette/Nette/Latte/Macros/MacroSet.php +++ /dev/null @@ -1,143 +0,0 @@ -compiler = $compiler; - } - - - - public function addMacro($name, $begin, $end = NULL, $attr = NULL) - { - $this->macros[$name] = array($begin, $end, $attr); - $this->compiler->addMacro($name, $this); - return $this; - } - - - - public static function install(Latte\Compiler $compiler) - { - return new static($compiler); - } - - - - /** - * Initializes before template parsing. - * @return void - */ - public function initialize() - { - } - - - - /** - * Finishes template parsing. - * @return array(prolog, epilog) - */ - public function finalize() - { - } - - - - /** - * New node is found. - * @return bool - */ - public function nodeOpened(MacroNode $node) - { - if ($this->macros[$node->name][2] && $node->htmlNode) { - $node->isEmpty = TRUE; - $this->compiler->setContext(Latte\Compiler::CONTEXT_DOUBLE_QUOTED); - $res = $this->compile($node, $this->macros[$node->name][2]); - $this->compiler->setContext(NULL); - if (!$node->attrCode) { - $node->attrCode = ""; - } - } else { - $node->isEmpty = !isset($this->macros[$node->name][1]); - $res = $this->compile($node, $this->macros[$node->name][0]); - if (!$node->openingCode) { - $node->openingCode = ""; - } - } - return $res !== FALSE; - } - - - - /** - * Node is closed. - * @return void - */ - public function nodeClosed(MacroNode $node) - { - $res = $this->compile($node, $this->macros[$node->name][1]); - if (!$node->closingCode) { - $node->closingCode = ""; - } - } - - - - /** - * Generates code. - * @return string - */ - private function compile(MacroNode $node, $def) - { - $node->tokenizer->reset(); - $writer = Latte\PhpWriter::using($node, $this->compiler); - if (is_string($def)/*5.2* && substr($def, 0, 1) !== "\0"*/) { - return $writer->write($def); - } else { - return Nette\Callback::create($def)->invoke($node, $writer); - } - } - - - - /** - * @return Latte\Compiler - */ - public function getCompiler() - { - return $this->compiler; - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/Macros/UIMacros.php b/apigen/libs/Nette/Nette/Latte/Macros/UIMacros.php deleted file mode 100644 index b20ae4c5401..00000000000 --- a/apigen/libs/Nette/Nette/Latte/Macros/UIMacros.php +++ /dev/null @@ -1,520 +0,0 @@ -addMacro('include', array($me, 'macroInclude')); - $me->addMacro('includeblock', array($me, 'macroIncludeBlock')); - $me->addMacro('extends', array($me, 'macroExtends')); - $me->addMacro('layout', array($me, 'macroExtends')); - $me->addMacro('block', array($me, 'macroBlock'), array($me, 'macroBlockEnd')); - $me->addMacro('define', array($me, 'macroBlock'), array($me, 'macroBlockEnd')); - $me->addMacro('snippet', array($me, 'macroBlock'), array($me, 'macroBlockEnd')); - $me->addMacro('ifset', array($me, 'macroIfset'), 'endif'); - - $me->addMacro('widget', array($me, 'macroControl')); // deprecated - use control - $me->addMacro('control', array($me, 'macroControl')); - - $me->addMacro('href', NULL, NULL, function(MacroNode $node, PhpWriter $writer) use ($me) { - return ' ?> href="macroLink($node, $writer) . ' ?>"addMacro('plink', array($me, 'macroLink')); - $me->addMacro('link', array($me, 'macroLink')); - $me->addMacro('ifCurrent', array($me, 'macroIfCurrent'), 'endif'); // deprecated; use n:class="$presenter->linkCurrent ? ..." - - $me->addMacro('contentType', array($me, 'macroContentType')); - $me->addMacro('status', array($me, 'macroStatus')); - } - - - - /** - * Initializes before template parsing. - * @return void - */ - public function initialize() - { - $this->namedBlocks = array(); - $this->extends = NULL; - } - - - - /** - * Finishes template parsing. - * @return array(prolog, epilog) - */ - public function finalize() - { - // try close last block - try { - $this->getCompiler()->writeMacro('/block'); - } catch (CompileException $e) { - } - - $epilog = $prolog = array(); - - if ($this->namedBlocks) { - foreach ($this->namedBlocks as $name => $code) { - $func = '_lb' . substr(md5($this->getCompiler()->getTemplateId() . $name), 0, 10) . '_' . preg_replace('#[^a-z0-9_]#i', '_', $name); - $snippet = $name[0] === '_'; - $prolog[] = "//\n// block $name\n//\n" - . "if (!function_exists(\$_l->blocks[" . var_export($name, TRUE) . "][] = '$func')) { " - . "function $func(\$_l, \$_args) { " - . (PHP_VERSION_ID > 50208 ? 'extract($_args)' : 'foreach ($_args as $__k => $__v) $$__k = $__v') // PHP bug #46873 - . ($snippet ? '; $_control->validateControl(' . var_export(substr($name, 1), TRUE) . ')' : '') - . "\n?>$codenamedBlocks || $this->extends) { - $prolog[] = "// template extending and snippets support"; - - $prolog[] = '$_l->extends = ' - . ($this->extends ? $this->extends : 'empty($template->_extended) && isset($_control) && $_control instanceof Nette\Application\UI\Presenter ? $_control->findLayoutTemplateFile() : NULL') - . '; $template->_extended = $_extended = TRUE;'; - - $prolog[] = ' -if ($_l->extends) { - ' . ($this->namedBlocks ? 'ob_start();' : 'return Nette\Latte\Macros\CoreMacros::includeTemplate($_l->extends, get_defined_vars(), $template)->render();') . ' - -} elseif (!empty($_control->snippetMode)) { - return Nette\Latte\Macros\UIMacros::renderSnippets($_control, $_l, get_defined_vars()); -}'; - } else { - $prolog[] = ' -// snippets support -if (!empty($_control->snippetMode)) { - return Nette\Latte\Macros\UIMacros::renderSnippets($_control, $_l, get_defined_vars()); -}'; - } - - return array(implode("\n\n", $prolog), implode("\n", $epilog)); - } - - - - /********************* macros ****************d*g**/ - - - - /** - * {include #block} - */ - public function macroInclude(MacroNode $node, PhpWriter $writer) - { - $destination = $node->tokenizer->fetchWord(); // destination [,] [params] - if (substr($destination, 0, 1) !== '#') { - return FALSE; - } - - $destination = ltrim($destination, '#'); - $parent = $destination === 'parent'; - if ($destination === 'parent' || $destination === 'this') { - for ($item = $node->parentNode; $item && $item->name !== 'block' && !isset($item->data->name); $item = $item->parentNode); - if (!$item) { - throw new CompileException("Cannot include $destination block outside of any block."); - } - $destination = $item->data->name; - } - - $name = Strings::contains($destination, '$') ? $destination : var_export($destination, TRUE); - if (isset($this->namedBlocks[$destination]) && !$parent) { - $cmd = "call_user_func(reset(\$_l->blocks[$name]), \$_l, %node.array? + get_defined_vars())"; - } else { - $cmd = 'Nette\Latte\Macros\UIMacros::callBlock' . ($parent ? 'Parent' : '') . "(\$_l, $name, %node.array? + " . ($parent ? 'get_defined_vars' : '$template->getParameters') . '())'; - } - - if ($node->modifiers) { - return $writer->write("ob_start(); $cmd; echo %modify(ob_get_clean())"); - } else { - return $writer->write($cmd); - } - } - - - - /** - * {includeblock "file"} - */ - public function macroIncludeBlock(MacroNode $node, PhpWriter $writer) - { - return $writer->write('Nette\Latte\Macros\CoreMacros::includeTemplate(%node.word, %node.array? + get_defined_vars(), $_l->templates[%var])->render()', - $this->getCompiler()->getTemplateId()); - } - - - - /** - * {extends auto | none | $var | "file"} - */ - public function macroExtends(MacroNode $node, PhpWriter $writer) - { - if (!$node->args) { - throw new CompileException("Missing destination in {extends}"); - } - if (!empty($node->parentNode)) { - throw new CompileException("{extends} must be placed outside any macro."); - } - if ($this->extends !== NULL) { - throw new CompileException("Multiple {extends} declarations are not allowed."); - } - if ($node->args === 'none') { - $this->extends = 'FALSE'; - } elseif ($node->args === 'auto') { - $this->extends = '$_presenter->findLayoutTemplateFile()'; - } else { - $this->extends = $writer->write('%node.word%node.args'); - } - return; - } - - - - /** - * {block [[#]name]} - * {snippet [name [,]] [tag]} - * {define [#]name} - */ - public function macroBlock(MacroNode $node, PhpWriter $writer) - { - $name = $node->tokenizer->fetchWord(); - - if ($node->name === 'block' && $name === FALSE) { // anonymous block - return $node->modifiers === '' ? '' : 'ob_start()'; - } - - $node->data->name = $name = ltrim($name, '#'); - if ($name == NULL) { - if ($node->name !== 'snippet') { - throw new CompileException("Missing block name."); - } - - } elseif (Strings::contains($name, '$')) { // dynamic block/snippet - if ($node->name === 'snippet') { - for ($parent = $node->parentNode; $parent && $parent->name !== 'snippet'; $parent = $parent->parentNode); - if (!$parent) { - throw new CompileException("Dynamic snippets are allowed only inside static snippet."); - } - $parent->data->dynamic = TRUE; - $node->data->leave = TRUE; - $node->closingCode = ""; - - if ($node->htmlNode) { - $node->attrCode = $writer->write("getSnippetId({$writer->formatWord($name)})) . '\"' ?>"); - return $writer->write('ob_start()'); - } - $tag = trim($node->tokenizer->fetchWord(), '<>'); - $tag = $tag ? $tag : 'div'; - $node->closingCode .= "\n"; - return $writer->write("?>\n<$tag id=\"getSnippetId({$writer->formatWord($name)}) ?>\">data->leave = TRUE; - $fname = $writer->formatWord($name); - $node->closingCode = "name === 'define' ? '' : "call_user_func(reset(\$_l->blocks[$fname]), \$_l, get_defined_vars())") . " ?>"; - $func = '_lb' . substr(md5($this->getCompiler()->getTemplateId() . $name), 0, 10) . '_' . preg_replace('#[^a-z0-9_]#i', '_', $name); - return "\n\n//\n// block $name\n//\n" - . "if (!function_exists(\$_l->blocks[$fname]['{$this->getCompiler()->getTemplateId()}'] = '$func')) { " - . "function $func(\$_l, \$_args) { " - . (PHP_VERSION_ID > 50208 ? 'extract($_args)' : 'foreach ($_args as $__k => $__v) $$__k = $__v'); // PHP bug #46873 - } - } - - // static block/snippet - if ($node->name === 'snippet') { - $node->data->name = $name = '_' . $name; - } - - if (isset($this->namedBlocks[$name])) { - throw new CompileException("Cannot redeclare static block '$name'"); - } - - $prolog = $this->namedBlocks ? '' : "if (\$_l->extends) { ob_end_clean(); return Nette\\Latte\\Macros\\CoreMacros::includeTemplate(\$_l->extends, get_defined_vars(), \$template)->render(); }\n"; - $top = empty($node->parentNode); - $this->namedBlocks[$name] = TRUE; - - $include = 'call_user_func(reset($_l->blocks[%var]), $_l, ' . ($node->name === 'snippet' ? '$template->getParameters()' : 'get_defined_vars()') . ')'; - if ($node->modifiers) { - $include = "ob_start(); $include; echo %modify(ob_get_clean())"; - } - - if ($node->name === 'snippet') { - if ($node->htmlNode) { - $node->attrCode = $writer->write('getSnippetId(%var) . \'"\' ?>', (string) substr($name, 1)); - return $writer->write($prolog . $include, $name); - } - $tag = trim($node->tokenizer->fetchWord(), '<>'); - $tag = $tag ? $tag : 'div'; - return $writer->write("$prolog ?>\n<$tag id=\"getSnippetId(%var) ?>\">\nname === 'define') { - return $prolog; - - } else { - return $writer->write($prolog . $include, $name); - } - } - - - - /** - * {/block} - * {/snippet} - * {/define} - */ - public function macroBlockEnd(MacroNode $node, PhpWriter $writer) - { - if (isset($node->data->name)) { // block, snippet, define - if ($node->name === 'snippet' && $node->htmlNode && !$node->prefix // n:snippet -> n:inner-snippet - && preg_match("#^.*? n:\w+>\n?#s", $node->content, $m1) && preg_match("#[ \t]*<[^<]+$#sD", $node->content, $m2)) - { - $node->openingCode = $m1[0] . $node->openingCode; - $node->content = substr($node->content, strlen($m1[0]), -strlen($m2[0])); - $node->closingCode .= $m2[0]; - } - - if (empty($node->data->leave)) { - if (!empty($node->data->dynamic)) { - $node->content .= ''; - } - $this->namedBlocks[$node->data->name] = $tmp = rtrim(ltrim($node->content, "\n"), " \t"); - $node->content = substr_replace($node->content, $node->openingCode . "\n", strspn($node->content, "\n"), strlen($tmp)); - $node->openingCode = ""; - } - - } elseif ($node->modifiers) { // anonymous block with modifier - return $writer->write('echo %modify(ob_get_clean())'); - } - } - - - - /** - * {ifset #block} - */ - public function macroIfset(MacroNode $node, PhpWriter $writer) - { - if (!Strings::contains($node->args, '#')) { - return FALSE; - } - $list = array(); - while (($name = $node->tokenizer->fetchWord()) !== FALSE) { - $list[] = $name[0] === '#' ? '$_l->blocks["' . substr($name, 1) . '"]' : $name; - } - return 'if (isset(' . implode(', ', $list) . ')):'; - } - - - - /** - * {control name[:method] [params]} - */ - public function macroControl(MacroNode $node, PhpWriter $writer) - { - $pair = $node->tokenizer->fetchWord(); - if ($pair === FALSE) { - throw new CompileException("Missing control name in {control}"); - } - $pair = explode(':', $pair, 2); - $name = $writer->formatWord($pair[0]); - $method = isset($pair[1]) ? ucfirst($pair[1]) : ''; - $method = Strings::match($method, '#^\w*$#') ? "render$method" : "{\"render$method\"}"; - $param = $writer->formatArray(); - if (!Strings::contains($node->args, '=>')) { - $param = substr($param, 6, -1); // removes array() - } - return ($name[0] === '$' ? "if (is_object($name)) \$_ctrl = $name; else " : '') - . '$_ctrl = $_control->getComponent(' . $name . '); ' - . 'if ($_ctrl instanceof Nette\Application\UI\IRenderable) $_ctrl->validateControl(); ' - . "\$_ctrl->$method($param)"; - } - - - - /** - * {link destination [,] [params]} - * {plink destination [,] [params]} - * n:href="destination [,] [params]" - */ - public function macroLink(MacroNode $node, PhpWriter $writer) - { - return $writer->write('echo %escape(%modify(' . ($node->name === 'plink' ? '$_presenter' : '$_control') . '->link(%node.word, %node.array?)))'); - } - - - - /** - * {ifCurrent destination [,] [params]} - */ - public function macroIfCurrent(MacroNode $node, PhpWriter $writer) - { - return $writer->write(($node->args ? 'try { $_presenter->link(%node.word, %node.array?); } catch (Nette\Application\UI\InvalidLinkException $e) {}' : '') - . '; if ($_presenter->getLastCreatedRequestFlag("current")):'); - } - - - - /** - * {contentType ...} - */ - public function macroContentType(MacroNode $node, PhpWriter $writer) - { - if (Strings::contains($node->args, 'xhtml')) { - $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_XHTML); - - } elseif (Strings::contains($node->args, 'html')) { - $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_HTML); - - } elseif (Strings::contains($node->args, 'xml')) { - $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_XML); - - } elseif (Strings::contains($node->args, 'javascript')) { - $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_JS); - - } elseif (Strings::contains($node->args, 'css')) { - $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_CSS); - - } elseif (Strings::contains($node->args, 'calendar')) { - $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_ICAL); - - } else { - $this->getCompiler()->setContentType(Latte\Compiler::CONTENT_TEXT); - } - - // temporary solution - if (Strings::contains($node->args, '/')) { - return $writer->write('$netteHttpResponse->setHeader("Content-Type", %var)', $node->args); - } - } - - - - /** - * {status ...} - */ - public function macroStatus(MacroNode $node, PhpWriter $writer) - { - return $writer->write((substr($node->args, -1) === '?' ? 'if (!$netteHttpResponse->isSent()) ' : '') . - '$netteHttpResponse->setCode(%var)', (int) $node->args - ); - } - - - - /********************* run-time writers ****************d*g**/ - - - - /** - * Calls block. - * @return void - */ - public static function callBlock(\stdClass $context, $name, array $params) - { - if (empty($context->blocks[$name])) { - throw new Nette\InvalidStateException("Cannot include undefined block '$name'."); - } - $block = reset($context->blocks[$name]); - $block($context, $params); - } - - - - /** - * Calls parent block. - * @return void - */ - public static function callBlockParent(\stdClass $context, $name, array $params) - { - if (empty($context->blocks[$name]) || ($block = next($context->blocks[$name])) === FALSE) { - throw new Nette\InvalidStateException("Cannot include undefined parent block '$name'."); - } - $block($context, $params); - } - - - - public static function renderSnippets(Nette\Application\UI\Control $control, \stdClass $local, array $params) - { - $control->snippetMode = FALSE; - $payload = $control->getPresenter()->getPayload(); - if (isset($local->blocks)) { - foreach ($local->blocks as $name => $function) { - if ($name[0] !== '_' || !$control->isControlInvalid(substr($name, 1))) { - continue; - } - ob_start(); - $function = reset($function); - $snippets = $function($local, $params); - $payload->snippets[$id = $control->getSnippetId(substr($name, 1))] = ob_get_clean(); - if ($snippets) { - $payload->snippets += $snippets; - unset($payload->snippets[$id]); - } - } - } - $control->snippetMode = TRUE; - if ($control instanceof Nette\Application\UI\IRenderable) { - $queue = array($control); - do { - foreach (array_shift($queue)->getComponents() as $child) { - if ($child instanceof Nette\Application\UI\IRenderable) { - if ($child->isControlInvalid()) { - $child->snippetMode = TRUE; - $child->render(); - $child->snippetMode = FALSE; - } - } elseif ($child instanceof Nette\ComponentModel\IContainer) { - $queue[] = $child; - } - } - } while ($queue); - } - } - -} diff --git a/apigen/libs/Nette/Nette/Latte/Parser.php b/apigen/libs/Nette/Nette/Latte/Parser.php deleted file mode 100644 index df2f8b5824a..00000000000 --- a/apigen/libs/Nette/Nette/Latte/Parser.php +++ /dev/null @@ -1,405 +0,0 @@ - array('\\{(?![\\s\'"{}])', '\\}'), // {...} - 'double' => array('\\{\\{(?![\\s\'"{}])', '\\}\\}'), // {{...}} - 'asp' => array('<%\s*', '\s*%>'), /* <%...%> */ - 'python' => array('\\{[{%]\s*', '\s*[%}]\\}'), // {% ... %} | {{ ... }} - 'off' => array('[^\x00-\xFF]', ''), - ); - - /** @var string */ - private $macroRe; - - /** @var string source template */ - private $input; - - /** @var Token[] */ - private $output; - - /** @var int position on source template */ - private $offset; - - /** @var array */ - private $context; - - /** @var string */ - private $lastHtmlTag; - - /** @var string used by filter() */ - private $syntaxEndTag; - - /** @var bool */ - private $xmlMode; - - /** @internal states */ - const CONTEXT_TEXT = 'text', - CONTEXT_CDATA = 'cdata', - CONTEXT_TAG = 'tag', - CONTEXT_ATTRIBUTE = 'attribute', - CONTEXT_NONE = 'none', - CONTEXT_COMMENT = 'comment'; - - - - /** - * Process all {macros} and . - * @param string - * @return array - */ - public function parse($input) - { - if (substr($input, 0, 3) === "\xEF\xBB\xBF") { // BOM - $input = substr($input, 3); - } - if (!Strings::checkEncoding($input)) { - throw new Nette\InvalidArgumentException('Template is not valid UTF-8 stream.'); - } - $input = str_replace("\r\n", "\n", $input); - $this->input = $input; - $this->output = array(); - $this->offset = 0; - - $this->setSyntax($this->defaultSyntax); - $this->setContext(self::CONTEXT_TEXT); - $this->lastHtmlTag = $this->syntaxEndTag = NULL; - - while ($this->offset < strlen($input)) { - $matches = $this->{"context".$this->context[0]}(); - - if (!$matches) { // EOF - break; - - } elseif (!empty($matches['comment'])) { // {* *} - $this->addToken(Token::COMMENT, $matches[0]); - - } elseif (!empty($matches['macro'])) { // {macro} - $token = $this->addToken(Token::MACRO_TAG, $matches[0]); - list($token->name, $token->value, $token->modifiers) = $this->parseMacroTag($matches['macro']); - } - - $this->filter(); - } - - if ($this->offset < strlen($input)) { - $this->addToken(Token::TEXT, substr($this->input, $this->offset)); - } - return $this->output; - } - - - - /** - * Handles CONTEXT_TEXT. - */ - private function contextText() - { - $matches = $this->match('~ - (?:(?<=\n|^)[ \t]*)?<(?P/?)(?P[a-z0-9:]+)| ## begin of HTML tag !--)| ## begin of HTML comment $levels['#'] + $top = 0 + 1 = 1 -->

...

- '*' => 1, - '=' => 2, - '-' => 3, - ); - - /** @var array used ID's */ - private $usedID; - - - - public function __construct($texy) - { - $this->texy = $texy; - - $texy->addHandler('heading', array($this, 'solve')); - $texy->addHandler('beforeParse', array($this, 'beforeParse')); - $texy->addHandler('afterParse', array($this, 'afterParse')); - - $texy->registerBlockPattern( - array($this, 'patternUnderline'), - '#^(\S.*)'.TEXY_MODIFIER_H.'?\n' - . '(\#{3,}|\*{3,}|={3,}|-{3,})$#mU', - 'heading/underlined' - ); - - $texy->registerBlockPattern( - array($this, 'patternSurround'), - '#^(\#{2,}+|={2,}+)(.+)'.TEXY_MODIFIER_H.'?()$#mU', - 'heading/surrounded' - ); - } - - - - public function beforeParse() - { - $this->title = NULL; - $this->usedID = array(); - $this->TOC = array(); - } - - - - /** - * @param Texy - * @param TexyHtml - * @param bool - * @return void - */ - public function afterParse($texy, $DOM, $isSingleLine) - { - if ($isSingleLine) return; - - if ($this->balancing === self::DYNAMIC) { - $top = $this->top; - $map = array(); - $min = 100; - foreach ($this->TOC as $item) - { - $level = $item['level']; - if ($item['type'] === 'surrounded') { - $min = min($level, $min); - $top = $this->top - $min; - - } elseif ($item['type'] === 'underlined') { - $map[$level] = $level; - } - } - - asort($map); - $map = array_flip(array_values($map)); - } - - foreach ($this->TOC as $key => $item) - { - if ($this->balancing === self::DYNAMIC) { - if ($item['type'] === 'surrounded') { - $level = $item['level'] + $top; - - } elseif ($item['type'] === 'underlined') { - $level = $map[$item['level']] + $this->top; - - } else { - $level = $item['level']; - } - - $item['el']->setName('h' . min(6, max(1, $level))); - $this->TOC[$key]['level'] = $level; - } - - if ($this->generateID && empty($item['el']->attrs['id'])) { - $title = trim($item['el']->toText($this->texy)); - if ($title !== '') { - $this->TOC[$key]['title'] = $title; - $id = $this->idPrefix . Texy::webalize($title); - $counter = ''; - if (isset($this->usedID[$id . $counter])) { - $counter = 2; - while (isset($this->usedID[$id . '-' . $counter])) $counter++; - $id .= '-' . $counter; - } - $this->usedID[$id] = TRUE; - $item['el']->attrs['id'] = $id; - } - } - } - - // document title - if ($this->title === NULL && count($this->TOC)) { - $item = reset($this->TOC); - $this->title = isset($item['title']) ? $item['title'] : trim($item['el']->toText($this->texy)); - } - } - - - - /** - * Callback for underlined heading. - * - * Heading .(title)[class]{style}> - * ------------------------------- - * - * @param TexyBlockParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternUnderline($parser, $matches) - { - list(, $mContent, $mMod, $mLine) = $matches; - // $matches: - // [1] => ... - // [2] => .(title)[class]{style}<> - // [3] => ... - - $mod = new TexyModifier($mMod); - $level = $this->levels[$mLine[0]]; - return $this->texy->invokeAroundHandlers('heading', $parser, array($level, $mContent, $mod, FALSE)); - } - - - - /** - * Callback for surrounded heading. - * - * ### Heading .(title)[class]{style}> - * - * @param TexyBlockParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternSurround($parser, $matches) - { - list(, $mLine, $mContent, $mMod) = $matches; - // [1] => ### - // [2] => ... - // [3] => .(title)[class]{style}<> - - $mod = new TexyModifier($mMod); - $level = min(7, max(2, strlen($mLine))); - $level = $this->moreMeansHigher ? 7 - $level : $level - 2; - $mContent = rtrim($mContent, $mLine[0] . ' '); - return $this->texy->invokeAroundHandlers('heading', $parser, array($level, $mContent, $mod, TRUE)); - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param int 0..5 - * @param string - * @param TexyModifier - * @param bool - * @return TexyHtml - */ - public function solve($invocation, $level, $content, $mod, $isSurrounded) - { - // as fixed balancing, for block/texysource & correct decorating - $el = TexyHtml::el('h' . min(6, max(1, $level + $this->top))); - $mod->decorate($this->texy, $el); - - $el->parseLine($this->texy, trim($content)); - - $this->TOC[] = array( - 'el' => $el, - 'level' => $level, - 'type' => $isSurrounded ? 'surrounded' : 'underlined', - ); - - return $el; - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyHorizLineModule.php b/apigen/libs/Texy/texy/modules/TexyHorizLineModule.php deleted file mode 100644 index f39522221e2..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyHorizLineModule.php +++ /dev/null @@ -1,86 +0,0 @@ - NULL, - '*' => NULL, - ); - - - - public function __construct($texy) - { - $this->texy = $texy; - - $texy->addHandler('horizline', array($this, 'solve')); - - $texy->registerBlockPattern( - array($this, 'pattern'), - '#^(\*{3,}|-{3,})\ *'.TEXY_MODIFIER.'?()$#mU', - 'horizline' - ); - } - - - - /** - * Callback for: -------. - * - * @param TexyBlockParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml - */ - public function pattern($parser, $matches) - { - list(, $mType, $mMod) = $matches; - // [1] => --- - // [2] => .(title)[class]{style}<> - - $mod = new TexyModifier($mMod); - return $this->texy->invokeAroundHandlers('horizline', $parser, array($mType, $mod)); - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param string - * @param TexyModifier - * @return TexyHtml - */ - public function solve($invocation, $type, $modifier) - { - $el = TexyHtml::el('hr'); - $modifier->decorate($invocation->texy, $el); - - $class = $this->classes[ $type[0] ]; - if ($class && !isset($modifier->classes[$class])) { - $el->attrs['class'][] = $class; - } - - return $el; - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyHtmlModule.php b/apigen/libs/Texy/texy/modules/TexyHtmlModule.php deleted file mode 100644 index 052cb8269dd..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyHtmlModule.php +++ /dev/null @@ -1,281 +0,0 @@ -texy = $texy; - - $texy->addHandler('htmlComment', array($this, 'solveComment')); - $texy->addHandler('htmlTag', array($this, 'solveTag')); - - $texy->registerLinePattern( - array($this, 'patternTag'), - '#<(/?)([a-z][a-z0-9_:-]*)((?:\s+[a-z0-9:-]+|=\s*"[^"'.TEXY_MARK.']*"|=\s*\'[^\''.TEXY_MARK.']*\'|=[^\s>'.TEXY_MARK.']+)*)\s*(/?)>#isu', - 'html/tag' - ); - - $texy->registerLinePattern( - array($this, 'patternComment'), - '##is', - 'html/comment' - ); - } - - - - /** - * Callback for: . - * - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternComment($parser, $matches) - { - list(, $mComment) = $matches; - return $this->texy->invokeAroundHandlers('htmlComment', $parser, array($mComment)); - } - - - - /** - * Callback for: . - * - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternTag($parser, $matches) - { - list(, $mEnd, $mTag, $mAttr, $mEmpty) = $matches; - // [1] => / - // [2] => tag - // [3] => attributes - // [4] => / - - $tx = $this->texy; - - $isStart = $mEnd !== '/'; - $isEmpty = $mEmpty === '/'; - if (!$isEmpty && substr($mAttr, -1) === '/') { // uvizlo v $mAttr? - $mAttr = substr($mAttr, 0, -1); - $isEmpty = TRUE; - } - - // error - can't close empty element - if ($isEmpty && !$isStart) - return FALSE; - - - // error - end element with atttrs - $mAttr = trim(strtr($mAttr, "\n", ' ')); - if ($mAttr && !$isStart) - return FALSE; - - - $el = TexyHtml::el($mTag); - - if ($isStart) { - // parse attributes - $matches2 = NULL; - preg_match_all( - '#([a-z0-9:-]+)\s*(?:=\s*(\'[^\']*\'|"[^"]*"|[^\'"\s]+))?()#isu', - $mAttr, - $matches2, - PREG_SET_ORDER - ); - - foreach ($matches2 as $m) { - $key = strtolower($m[1]); - $value = $m[2]; - if ($value == NULL) $el->attrs[$key] = TRUE; - elseif ($value{0} === '\'' || $value{0} === '"') $el->attrs[$key] = Texy::unescapeHtml(substr($value, 1, -1)); - else $el->attrs[$key] = Texy::unescapeHtml($value); - } - } - - $res = $tx->invokeAroundHandlers('htmlTag', $parser, array($el, $isStart, $isEmpty)); - - if ($res instanceof TexyHtml) { - return $tx->protect($isStart ? $res->startTag() : $res->endTag(), $res->getContentType()); - } - - return $res; - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param TexyHtml element - * @param bool is start tag? - * @param bool is empty? - * @return TexyHtml|string|FALSE - */ - public function solveTag($invocation, TexyHtml $el, $isStart, $forceEmpty = NULL) - { - $tx = $this->texy; - - // tag & attibutes - $allowedTags = $tx->allowedTags; // speed-up - if (!$allowedTags) - return FALSE; // all tags are disabled - - // convert case - $name = $el->getName(); - $lower = strtolower($name); - if (isset($tx->dtd[$lower]) || $name === strtoupper($name)) { - // complete UPPER convert to lower - $name = $lower; - $el->setName($name); - } - - if (is_array($allowedTags)) { - if (!isset($allowedTags[$name])) return FALSE; - $allowedAttrs = $allowedTags[$name]; // allowed attrs - - } else { - // allowedTags === Texy::ALL - if ($forceEmpty) $el->setName($name, TRUE); - $allowedAttrs = Texy::ALL; // all attrs are allowed - } - - // end tag? we are finished - if (!$isStart) { - return $el; - } - - $elAttrs = & $el->attrs; - - // process attributes - if (!$allowedAttrs) { - $elAttrs = array(); - - } elseif (is_array($allowedAttrs)) { - - // skip disabled - $allowedAttrs = array_flip($allowedAttrs); - foreach ($elAttrs as $key => $foo) - if (!isset($allowedAttrs[$key])) unset($elAttrs[$key]); - } - - // apply allowedClasses - $tmp = $tx->_classes; // speed-up - if (isset($elAttrs['class'])) { - if (is_array($tmp)) { - $elAttrs['class'] = explode(' ', $elAttrs['class']); - foreach ($elAttrs['class'] as $key => $value) - if (!isset($tmp[$value])) unset($elAttrs['class'][$key]); // id & class are case-sensitive - - } elseif ($tmp !== Texy::ALL) { - $elAttrs['class'] = NULL; - } - } - - // apply allowedClasses for ID - if (isset($elAttrs['id'])) { - if (is_array($tmp)) { - if (!isset($tmp['#' . $elAttrs['id']])) $elAttrs['id'] = NULL; - } elseif ($tmp !== Texy::ALL) { - $elAttrs['id'] = NULL; - } - } - - // apply allowedStyles - if (isset($elAttrs['style'])) { - $tmp = $tx->_styles; // speed-up - if (is_array($tmp)) { - $styles = explode(';', $elAttrs['style']); - $elAttrs['style'] = NULL; - foreach ($styles as $value) { - $pair = explode(':', $value, 2); - $prop = trim($pair[0]); - if (isset($pair[1]) && isset($tmp[strtolower($prop)])) // CSS is case-insensitive - $elAttrs['style'][$prop] = $pair[1]; - } - } elseif ($tmp !== Texy::ALL) { - $elAttrs['style'] = NULL; - } - } - - if ($name === 'img') { - if (!isset($elAttrs['src'])) return FALSE; - - if (!$tx->checkURL($elAttrs['src'], Texy::FILTER_IMAGE)) return FALSE; - - $tx->summary['images'][] = $elAttrs['src']; - - } elseif ($name === 'a') { - if (!isset($elAttrs['href']) && !isset($elAttrs['name']) && !isset($elAttrs['id'])) return FALSE; - if (isset($elAttrs['href'])) { - if ($tx->linkModule->forceNoFollow && strpos($elAttrs['href'], '//') !== FALSE) { - if (isset($elAttrs['rel'])) $elAttrs['rel'] = (array) $elAttrs['rel']; - $elAttrs['rel'][] = 'nofollow'; - } - - if (!$tx->checkURL($elAttrs['href'], Texy::FILTER_ANCHOR)) return FALSE; - - $tx->summary['links'][] = $elAttrs['href']; - } - - } elseif (preg_match('#^h[1-6]#i', $name)) { - $tx->headingModule->TOC[] = array( - 'el' => $el, - 'level' => (int) substr($name, 1), - 'type' => 'html', - ); - } - - $el->validateAttrs($tx->dtd); - - return $el; - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param string - * @return string - */ - public function solveComment($invocation, $content) - { - if (!$this->passComment) return ''; - - // sanitize comment - $content = preg_replace('#-{2,}#', '-', $content); - $content = trim($content, '-'); - - return $this->texy->protect('', Texy::CONTENT_MARKUP); - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyHtmlOutputModule.php b/apigen/libs/Texy/texy/modules/TexyHtmlOutputModule.php deleted file mode 100644 index 92ff55b77fd..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyHtmlOutputModule.php +++ /dev/null @@ -1,311 +0,0 @@ -texy = $texy; - $texy->addHandler('postProcess', array($this, 'postProcess')); - } - - - - /** - * Converts ... ... . - * into ... ... - */ - public function postProcess($texy, & $s) - { - $this->space = $this->baseIndent; - $this->tagStack = array(); - $this->tagUsed = array(); - $this->xml = $texy->getOutputMode() & Texy::XML; - - // special "base content" - $this->baseDTD = $texy->dtd['div'][1] + $texy->dtd['html'][1] /*+ $texy->dtd['head'][1]*/ + $texy->dtd['body'][1] + array('html'=>1); - - // wellform and reformat - $s = preg_replace_callback( - '#(.*)<(?:(!--.*--)|(/?)([a-z][a-z0-9._:-]*)(|[ \n].*)\s*(/?))>()#Uis', - array($this, 'cb'), - $s . '' - ); - - // empty out stack - foreach ($this->tagStack as $item) $s .= $item['close']; - - // right trim - $s = preg_replace("#[\t ]+(\n|\r|$)#", '$1', $s); // right trim - - // join double \r to single \n - $s = str_replace("\r\r", "\n", $s); - $s = strtr($s, "\r", "\n"); - - // greedy chars - $s = preg_replace("#\\x07 *#", '', $s); - // back-tabs - $s = preg_replace("#\\t? *\\x08#", '', $s); - - // line wrap - if ($this->lineWrap > 0) { - $s = preg_replace_callback( - '#^(\t*)(.*)$#m', - array($this, 'wrap'), - $s - ); - } - - // remove HTML 4.01 optional end tags - if (!$this->xml && $this->removeOptional) { - $s = preg_replace('#\\s*#u', '', $s); - } - } - - - - /** - * Callback function: | | .... - * @return string - */ - private function cb($matches) - { - // html tag - list(, $mText, $mComment, $mEnd, $mTag, $mAttr, $mEmpty) = $matches; - // [1] => text - // [1] => !-- comment -- - // [2] => / - // [3] => TAG - // [4] => ... (attributes) - // [5] => / (empty) - - $s = ''; - - // phase #1 - stuff between tags - if ($mText !== '') { - $item = reset($this->tagStack); - // text not allowed? - if ($item && !isset($item['dtdContent']['%DATA'])) { } - - // inside pre & textarea preserve spaces - elseif (!empty($this->tagUsed['pre']) || !empty($this->tagUsed['textarea']) || !empty($this->tagUsed['script'])) - $s = Texy::freezeSpaces($mText); - - // otherwise shrink multiple spaces - else $s = preg_replace('#[ \n]+#', ' ', $mText); - } - - - // phase #2 - HTML comment - if ($mComment) return $s . '<' . Texy::freezeSpaces($mComment) . '>'; - - - // phase #3 - HTML tag - $mEmpty = $mEmpty || isset(TexyHtml::$emptyElements[$mTag]); - if ($mEmpty && $mEnd) return $s; // bad tag; /end/ - - - if ($mEnd) { // end tag - - // has start tag? - if (empty($this->tagUsed[$mTag])) return $s; - - // autoclose tags - $tmp = array(); - $back = TRUE; - foreach ($this->tagStack as $i => $item) - { - $tag = $item['tag']; - $s .= $item['close']; - $this->space -= $item['indent']; - $this->tagUsed[$tag]--; - $back = $back && isset(TexyHtml::$inlineElements[$tag]); - unset($this->tagStack[$i]); - if ($tag === $mTag) break; - array_unshift($tmp, $item); - } - - if (!$back || !$tmp) return $s; - - // allowed-check (nejspis neni ani potreba) - $item = reset($this->tagStack); - if ($item) $dtdContent = $item['dtdContent']; - else $dtdContent = $this->baseDTD; - if (!isset($dtdContent[$tmp[0]['tag']])) return $s; - - // autoopen tags - foreach ($tmp as $item) - { - $s .= $item['open']; - $this->space += $item['indent']; - $this->tagUsed[$item['tag']]++; - array_unshift($this->tagStack, $item); - } - - - } else { // start tag - - $dtdContent = $this->baseDTD; - - if (!isset($this->texy->dtd[$mTag])) { - // unknown (non-html) tag - $allowed = TRUE; - $item = reset($this->tagStack); - if ($item) $dtdContent = $item['dtdContent']; - - - } else { - // optional end tag closing - foreach ($this->tagStack as $i => $item) - { - // is tag allowed here? - $dtdContent = $item['dtdContent']; - if (isset($dtdContent[$mTag])) break; - - $tag = $item['tag']; - - // auto-close hidden, optional and inline tags - if ($item['close'] && (!isset(TexyHtml::$optionalEnds[$tag]) && !isset(TexyHtml::$inlineElements[$tag]))) break; - - // close it - $s .= $item['close']; - $this->space -= $item['indent']; - $this->tagUsed[$tag]--; - unset($this->tagStack[$i]); - $dtdContent = $this->baseDTD; - } - - // is tag allowed in this content? - $allowed = isset($dtdContent[$mTag]); - - // check deep element prohibitions - if ($allowed && isset(TexyHtml::$prohibits[$mTag])) { - foreach (TexyHtml::$prohibits[$mTag] as $pTag) - if (!empty($this->tagUsed[$pTag])) { $allowed = FALSE; break; } - } - } - - // empty elements se neukladaji do zasobniku - if ($mEmpty) { - if (!$allowed) return $s; - - if ($this->xml) $mAttr .= " /"; - - $indent = $this->indent && empty($this->tagUsed['pre']) && empty($this->tagUsed['textarea']); - - if ($indent && $mTag === 'br') - // formatting exception - return rtrim($s) . '<' . $mTag . $mAttr . ">\n" . str_repeat("\t", max(0, $this->space - 1)) . "\x07"; - - if ($indent && !isset(TexyHtml::$inlineElements[$mTag])) { - $space = "\r" . str_repeat("\t", $this->space); - return $s . $space . '<' . $mTag . $mAttr . '>' . $space; - } - - return $s . '<' . $mTag . $mAttr . '>'; - } - - $open = NULL; - $close = NULL; - $indent = 0; - - /* - if (!isset(TexyHtml::$inlineElements[$mTag])) { - // block tags always decorate with \n - $s .= "\n"; - $close = "\n"; - } - */ - - if ($allowed) { - $open = '<' . $mTag . $mAttr . '>'; - - // receive new content (ins & del are special cases) - if (!empty($this->texy->dtd[$mTag][1])) $dtdContent = $this->texy->dtd[$mTag][1]; - - // format output - if ($this->indent && !isset(TexyHtml::$inlineElements[$mTag])) { - $close = "\x08" . '' . "\n" . str_repeat("\t", $this->space); - $s .= "\n" . str_repeat("\t", $this->space++) . $open . "\x07"; - $indent = 1; - } else { - $close = ''; - $s .= $open; - } - - // TODO: problematic formatting of select / options, object / params - } - - - // open tag, put to stack, increase counter - $item = array( - 'tag' => $mTag, - 'open' => $open, - 'close' => $close, - 'dtdContent' => $dtdContent, - 'indent' => $indent, - ); - array_unshift($this->tagStack, $item); - $tmp = &$this->tagUsed[$mTag]; $tmp++; - } - - return $s; - } - - - - /** - * Callback function: wrap lines. - * @return string - */ - private function wrap($m) - { - list(, $space, $s) = $m; - return $space . wordwrap($s, $this->lineWrap, "\n" . $space); - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyImageModule.php b/apigen/libs/Texy/texy/modules/TexyImageModule.php deleted file mode 100644 index ffd7acbc75e..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyImageModule.php +++ /dev/null @@ -1,372 +0,0 @@ -texy = $texy; - - $texy->allowed['image/definition'] = TRUE; - $texy->addHandler('image', array($this, 'solve')); - $texy->addHandler('beforeParse', array($this, 'beforeParse')); - - // [*image*]:LINK - $texy->registerLinePattern( - array($this, 'patternImage'), - '#'.TEXY_IMAGE.TEXY_LINK_N.'??()#Uu', - 'image' - ); - } - - - - /** - * Text pre-processing. - * @param Texy - * @param string - * @return void - */ - public function beforeParse($texy, & $text) - { - if (!empty($texy->allowed['image/definition'])) { - // [*image*]: urls .(title)[class]{style} - $text = preg_replace_callback( - '#^\[\*([^\n]+)\*\]:\ +(.+)\ *'.TEXY_MODIFIER.'?\s*()$#mUu', - array($this, 'patternReferenceDef'), - $text - ); - } - } - - - - /** - * Callback for: [*image*]: urls .(title)[class]{style}. - * - * @param array regexp matches - * @return string - */ - private function patternReferenceDef($matches) - { - list(, $mRef, $mURLs, $mMod) = $matches; - // [1] => [* (reference) *] - // [2] => urls - // [3] => .(title)[class]{style}<> - - $image = $this->factoryImage($mURLs, $mMod, FALSE); - $this->addReference($mRef, $image); - return ''; - } - - - - /** - * Callback for [* small.jpg 80x13 | small-over.jpg | big.jpg .(alternative text)[class]{style}>]:LINK. - * - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternImage($parser, $matches) - { - list(, $mURLs, $mMod, $mAlign, $mLink) = $matches; - // [1] => URLs - // [2] => .(title)[class]{style}<> - // [3] => * < > - // [4] => url | [ref] | [*image*] - - $tx = $this->texy; - - $image = $this->factoryImage($mURLs, $mMod.$mAlign); - - if ($mLink) { - if ($mLink === ':') { - $link = new TexyLink($image->linkedURL === NULL ? $image->URL : $image->linkedURL); - $link->raw = ':'; - $link->type = TexyLink::IMAGE; - } else { - $link = $tx->linkModule->factoryLink($mLink, NULL, NULL); - } - } else $link = NULL; - - return $tx->invokeAroundHandlers('image', $parser, array($image, $link)); - } - - - - /** - * Adds new named reference to image. - * - * @param string reference name - * @param TexyImage - * @return void - */ - public function addReference($name, TexyImage $image) - { - $image->name = TexyUtf::strtolower($name); - $this->references[$image->name] = $image; - } - - - - /** - * Returns named reference. - * - * @param string reference name - * @return TexyImage reference descriptor (or FALSE) - */ - public function getReference($name) - { - $name = TexyUtf::strtolower($name); - if (isset($this->references[$name])) - return clone $this->references[$name]; - - return FALSE; - } - - - - /** - * Parses image's syntax. - * @param string input: small.jpg 80x13 | small-over.jpg | linked.jpg - * @param string - * @param bool - * @return TexyImage - */ - public function factoryImage($content, $mod, $tryRef = TRUE) - { - $image = $tryRef ? $this->getReference(trim($content)) : FALSE; - - if (!$image) { - $tx = $this->texy; - $content = explode('|', $content); - $image = new TexyImage; - - // dimensions - $matches = NULL; - if (preg_match('#^(.*) (\d+|\?) *(X|x) *(\d+|\?) *()$#U', $content[0], $matches)) { - $image->URL = trim($matches[1]); - $image->asMax = $matches[3] === 'X'; - $image->width = $matches[2] === '?' ? NULL : (int) $matches[2]; - $image->height = $matches[4] === '?' ? NULL : (int) $matches[4]; - } else { - $image->URL = trim($content[0]); - } - - if (!$tx->checkURL($image->URL, Texy::FILTER_IMAGE)) $image->URL = NULL; - - // onmouseover image - if (isset($content[1])) { - $tmp = trim($content[1]); - if ($tmp !== '' && $tx->checkURL($tmp, Texy::FILTER_IMAGE)) $image->overURL = $tmp; - } - - // linked image - if (isset($content[2])) { - $tmp = trim($content[2]); - if ($tmp !== '' && $tx->checkURL($tmp, Texy::FILTER_ANCHOR)) $image->linkedURL = $tmp; - } - } - - $image->modifier->setProperties($mod); - return $image; - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param TexyImage - * @param TexyLink - * @return TexyHtml|FALSE - */ - public function solve($invocation, TexyImage $image, $link) - { - if ($image->URL == NULL) return FALSE; - - $tx = $this->texy; - - $mod = $image->modifier; - $alt = $mod->title; - $mod->title = NULL; - $hAlign = $mod->hAlign; - $mod->hAlign = NULL; - - $el = TexyHtml::el('img'); - $el->attrs['src'] = NULL; // trick - move to front - $mod->decorate($tx, $el); - $el->attrs['src'] = Texy::prependRoot($image->URL, $this->root); - if (!isset($el->attrs['alt'])) { - if ($alt !== NULL) $el->attrs['alt'] = $tx->typographyModule->postLine($alt); - else $el->attrs['alt'] = $this->defaultAlt; - } - - if ($hAlign) { - $var = $hAlign . 'Class'; // leftClass, rightClass - if (!empty($this->$var)) { - $el->attrs['class'][] = $this->$var; - - } elseif (empty($tx->alignClasses[$hAlign])) { - $el->attrs['style']['float'] = $hAlign; - - } else { - $el->attrs['class'][] = $tx->alignClasses[$hAlign]; - } - } - - if (!is_int($image->width) || !is_int($image->height) || $image->asMax) { - // autodetect fileRoot - if ($this->fileRoot === NULL && isset($_SERVER['SCRIPT_FILENAME'])) { - $this->fileRoot = dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $this->root; - } - - // detect dimensions - // absolute URL & security check for double dot - if (Texy::isRelative($image->URL) && strpos($image->URL, '..') === FALSE) { - $file = rtrim($this->fileRoot, '/\\') . '/' . $image->URL; - if (@is_file($file)) { // intentionally @ - $size = @getImageSize($file); // intentionally @ - if (is_array($size)) { - if ($image->asMax) { - $ratio = 1; - if (is_int($image->width)) $ratio = min($ratio, $image->width / $size[0]); - if (is_int($image->height)) $ratio = min($ratio, $image->height / $size[1]); - $image->width = round($ratio * $size[0]); - $image->height = round($ratio * $size[1]); - - } elseif (is_int($image->width)) { - $ratio = round($size[1] / $size[0] * $image->width); - $image->height = round($size[1] / $size[0] * $image->width); - - } elseif (is_int($image->height)) { - $image->width = round($size[0] / $size[1] * $image->height); - - } else { - $image->width = $size[0]; - $image->height = $size[1]; - } - } - } - } - } - - $el->attrs['width'] = $image->width; - $el->attrs['height'] = $image->height; - - // onmouseover actions generate - if ($image->overURL !== NULL) { - $overSrc = Texy::prependRoot($image->overURL, $this->root); - $el->attrs['onmouseover'] = 'this.src=\'' . addSlashes($overSrc) . '\''; - $el->attrs['onmouseout'] = 'this.src=\'' . addSlashes($el->attrs['src']) . '\''; - $el->attrs['onload'] = str_replace('%i', addSlashes($overSrc), $this->onLoad); - $tx->summary['preload'][] = $overSrc; - } - - $tx->summary['images'][] = $el->attrs['src']; - - if ($link) return $tx->linkModule->solve(NULL, $link, $el); - - return $el; - } - -} - - - - - - - -/** - * @package Texy - */ -final class TexyImage extends TexyObject -{ - /** @var string base image URL */ - public $URL; - - /** @var string on-mouse-over image URL */ - public $overURL; - - /** @var string anchored image URL */ - public $linkedURL; - - /** @var int optional image width */ - public $width; - - /** @var int optional image height */ - public $height; - - /** @var bool image width and height are maximal */ - public $asMax; - - /** @var TexyModifier */ - public $modifier; - - /** @var string reference name (if is stored as reference) */ - public $name; - - - - public function __construct() - { - $this->modifier = new TexyModifier; - } - - - - public function __clone() - { - if ($this->modifier) { - $this->modifier = clone $this->modifier; - } - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyLinkModule.php b/apigen/libs/Texy/texy/modules/TexyLinkModule.php deleted file mode 100644 index 16352282d8d..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyLinkModule.php +++ /dev/null @@ -1,493 +0,0 @@ -texy = $texy; - - $texy->allowed['link/definition'] = TRUE; - $texy->addHandler('newReference', array($this, 'solveNewReference')); - $texy->addHandler('linkReference', array($this, 'solve')); - $texy->addHandler('linkEmail', array($this, 'solveUrlEmail')); - $texy->addHandler('linkURL', array($this, 'solveUrlEmail')); - $texy->addHandler('beforeParse', array($this, 'beforeParse')); - - // [reference] - $texy->registerLinePattern( - array($this, 'patternReference'), - '#(\[[^\[\]\*\n'.TEXY_MARK.']+\])#U', - 'link/reference' - ); - - // direct url and email - $texy->registerLinePattern( - array($this, 'patternUrlEmail'), - '#(?<=^|[\s([<:\x17])(?:https?://|www\.|ftp://)[0-9.'.TEXY_CHAR.'-][/\d'.TEXY_CHAR.'+\.~%&?@=_:;\#,\x{ad}-]+[/\d'.TEXY_CHAR.'+~%?@=_\#]#u', - 'link/url', - '#(?:https?://|www\.|ftp://)#u' - ); - - $texy->registerLinePattern( - array($this, 'patternUrlEmail'), - '#(?<=^|[\s([<:\x17])'.TEXY_EMAIL.'#u', - 'link/email', - '#'.TEXY_EMAIL.'#u' - ); - } - - - - /** - * Text pre-processing. - * @param Texy - * @param string - * @return void - */ - public function beforeParse($texy, & $text) - { - self::$livelock = array(); - - // [la trine]: http://www.latrine.cz/ text odkazu .(title)[class]{style} - if (!empty($texy->allowed['link/definition'])) { - $text = preg_replace_callback( - '#^\[([^\[\]\#\?\*\n]+)\]: +(\S+)(\ .+)?'.TEXY_MODIFIER.'?\s*()$#mUu', - array($this, 'patternReferenceDef'), - $text - ); - } - } - - - - /** - * Callback for: [la trine]: http://www.latrine.cz/ text odkazu .(title)[class]{style}. - * - * @param array regexp matches - * @return string - */ - private function patternReferenceDef($matches) - { - list(, $mRef, $mLink, $mLabel, $mMod) = $matches; - // [1] => [ (reference) ] - // [2] => link - // [3] => ... - // [4] => .(title)[class]{style} - - $link = new TexyLink($mLink); - $link->label = trim($mLabel); - $link->modifier->setProperties($mMod); - $this->checkLink($link); - $this->addReference($mRef, $link); - return ''; - } - - - - /** - * Callback for: [ref]. - * - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternReference($parser, $matches) - { - list(, $mRef) = $matches; - // [1] => [ref] - - $tx = $this->texy; - $name = substr($mRef, 1, -1); - $link = $this->getReference($name); - - if (!$link) { - return $tx->invokeAroundHandlers('newReference', $parser, array($name)); - } - - $link->type = TexyLink::BRACKET; - - if ($link->label != '') { // NULL or '' - // prevent circular references - if (isset(self::$livelock[$link->name])) { - $content = $link->label; - } else { - self::$livelock[$link->name] = TRUE; - $el = TexyHtml::el(); - $lineParser = new TexyLineParser($tx, $el); - $lineParser->parse($link->label); - $content = $el->toString($tx); - unset(self::$livelock[$link->name]); - } - } else { - $content = $this->textualUrl($link); - $content = $this->texy->protect($content, Texy::CONTENT_TEXTUAL); - } - - return $tx->invokeAroundHandlers('linkReference', $parser, array($link, $content)); - } - - - - /** - * Callback for: http://davidgrudl.com david@grudl.com. - * - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternUrlEmail($parser, $matches, $name) - { - list($mURL) = $matches; - // [0] => URL - - $link = new TexyLink($mURL); - $this->checkLink($link); - - return $this->texy->invokeAroundHandlers( - $name === 'link/email' ? 'linkEmail' : 'linkURL', - $parser, - array($link) - ); - } - - - - /** - * Adds new named reference. - * - * @param string reference name - * @param TexyLink - * @return void - */ - public function addReference($name, TexyLink $link) - { - $link->name = TexyUtf::strtolower($name); - $this->references[$link->name] = $link; - } - - - - /** - * Returns named reference. - * - * @param string reference name - * @return TexyLink reference descriptor (or FALSE) - */ - public function getReference($name) - { - $name = TexyUtf::strtolower($name); - if (isset($this->references[$name])) { - return clone $this->references[$name]; - - } else { - $pos = strpos($name, '?'); - if ($pos === FALSE) $pos = strpos($name, '#'); - if ($pos !== FALSE) { // try to extract ?... #... part - $name2 = substr($name, 0, $pos); - if (isset($this->references[$name2])) { - $link = clone $this->references[$name2]; - $link->URL .= substr($name, $pos); - return $link; - } - } - } - - return FALSE; - } - - - - /** - * @param string - * @param string - * @param string - * @return TexyLink - */ - public function factoryLink($dest, $mMod, $label) - { - $tx = $this->texy; - $type = TexyLink::COMMON; - - // [ref] - if (strlen($dest)>1 && $dest{0} === '[' && $dest{1} !== '*') { - $type = TexyLink::BRACKET; - $dest = substr($dest, 1, -1); - $link = $this->getReference($dest); - - // [* image *] - } elseif (strlen($dest)>1 && $dest{0} === '[' && $dest{1} === '*') { - $type = TexyLink::IMAGE; - $dest = trim(substr($dest, 2, -2)); - $image = $tx->imageModule->getReference($dest); - if ($image) { - $link = new TexyLink($image->linkedURL === NULL ? $image->URL : $image->linkedURL); - $link->modifier = $image->modifier; - } - } - - if (empty($link)) { - $link = new TexyLink(trim($dest)); - $this->checkLink($link); - } - - if (strpos($link->URL, '%s') !== FALSE) { - $link->URL = str_replace('%s', urlencode($tx->stringToText($label)), $link->URL); - } - $link->modifier->setProperties($mMod); - $link->type = $type; - return $link; - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param TexyLink - * @param TexyHtml|string - * @return TexyHtml|string - */ - public function solve($invocation, $link, $content = NULL) - { - if ($link->URL == NULL) return $content; - - $tx = $this->texy; - - $el = TexyHtml::el('a'); - - if (empty($link->modifier)) { - $nofollow = $popup = FALSE; - } else { - $nofollow = isset($link->modifier->classes['nofollow']); - $popup = isset($link->modifier->classes['popup']); - unset($link->modifier->classes['nofollow'], $link->modifier->classes['popup']); - $el->attrs['href'] = NULL; // trick - move to front - $link->modifier->decorate($tx, $el); - } - - if ($link->type === TexyLink::IMAGE) { - // image - $el->attrs['href'] = Texy::prependRoot($link->URL, $tx->imageModule->linkedRoot); - $el->attrs['onclick'] = $this->imageOnClick; - - } else { - $el->attrs['href'] = Texy::prependRoot($link->URL, $this->root); - - // rel="nofollow" - if ($nofollow || ($this->forceNoFollow && strpos($el->attrs['href'], '//') !== FALSE)) - $el->attrs['rel'] = 'nofollow'; - } - - // popup on click - if ($popup) $el->attrs['onclick'] = $this->popupOnClick; - - if ($content !== NULL) $el->add($content); - - $tx->summary['links'][] = $el->attrs['href']; - - return $el; - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param TexyLink - * @return TexyHtml|string - */ - public function solveUrlEmail($invocation, $link) - { - $content = $this->textualUrl($link); - $content = $this->texy->protect($content, Texy::CONTENT_TEXTUAL); - return $this->solve(NULL, $link, $content); - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param string - * @return FALSE - */ - public function solveNewReference($invocation, $name) - { - // no change - return FALSE; - } - - - - /** - * Checks and corrects $URL. - * @param TexyLink - * @return void - */ - private function checkLink($link) - { - // remove soft hyphens; if not removed by Texy::process() - $link->URL = str_replace("\xC2\xAD", '', $link->URL); - - if (strncasecmp($link->URL, 'www.', 4) === 0) { - // special supported case - $link->URL = 'http://' . $link->URL; - - } elseif (preg_match('#'.TEXY_EMAIL.'$#Au', $link->URL)) { - // email - $link->URL = 'mailto:' . $link->URL; - - } elseif (!$this->texy->checkURL($link->URL, Texy::FILTER_ANCHOR)) { - $link->URL = NULL; - - } else { - $link->URL = str_replace('&', '&', $link->URL); // replace unwanted & - } - } - - - - /** - * Returns textual representation of URL. - * @param TexyLink - * @return string - */ - private function textualUrl($link) - { - if ($this->texy->obfuscateEmail && preg_match('#^'.TEXY_EMAIL.'$#u', $link->raw)) { // email - return str_replace('@', "@", $link->raw); - } - - if ($this->shorten && preg_match('#^(https?://|ftp://|www\.|/)#i', $link->raw)) { - - $raw = strncasecmp($link->raw, 'www.', 4) === 0 ? 'none://' . $link->raw : $link->raw; - - // parse_url() in PHP damages UTF-8 - use regular expression - if (!preg_match('~^(?:(?P[a-z]+):)?(?://(?P[^/?#]+))?(?P(?:/|^)(?!/)[^?#]*)?(?:\?(?P[^#]*))?(?:#(?P.*))?()$~', $raw, $parts)) { - return $link->raw; - } - - $res = ''; - if ($parts['scheme'] !== '' && $parts['scheme'] !== 'none') - $res .= $parts['scheme'] . '://'; - - if ($parts['host'] !== '') - $res .= $parts['host']; - - if ($parts['path'] !== '') - $res .= (iconv_strlen($parts['path'], 'UTF-8') > 16 ? ("/\xe2\x80\xa6" . iconv_substr($parts['path'], -12, 12, 'UTF-8')) : $parts['path']); - - if ($parts['query'] !== '') { - $res .= iconv_strlen($parts['query'], 'UTF-8') > 4 ? "?\xe2\x80\xa6" : ('?' . $parts['query']); - } elseif ($parts['fragment'] !== '') { - $res .= iconv_strlen($parts['fragment'], 'UTF-8') > 4 ? "#\xe2\x80\xa6" : ('#' . $parts['fragment']); - } - return $res; - } - - return $link->raw; - } - -} - - - - - - - - - -/** - * @package Texy - */ -final class TexyLink extends TexyObject -{ - /** @see $type */ - const - COMMON = 1, - BRACKET = 2, - IMAGE = 3; - - /** @var string URL in resolved form */ - public $URL; - - /** @var string URL as written in text */ - public $raw; - - /** @var TexyModifier */ - public $modifier; - - /** @var int how was link created? */ - public $type = TexyLink::COMMON; - - /** @var string optional label, used by references */ - public $label; - - /** @var string reference name (if is stored as reference) */ - public $name; - - - - public function __construct($URL) - { - $this->URL = $URL; - $this->raw = $URL; - $this->modifier = new TexyModifier; - } - - - - public function __clone() - { - if ($this->modifier) { - $this->modifier = clone $this->modifier; - } - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyListModule.php b/apigen/libs/Texy/texy/modules/TexyListModule.php deleted file mode 100644 index 11ff52e25ba..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyListModule.php +++ /dev/null @@ -1,255 +0,0 @@ - array('\*\ ', 0, ''), - '-' => array('[\x{2013}-](?![>-])',0, ''), - '+' => array('\+\ ', 0, ''), - '1.' => array('1\.\ ',/* not \d !*/ 1, '', '\d{1,3}\.\ '), - '1)' => array('\d{1,3}\)\ ', 1, ''), - 'I.' => array('I\.\ ', 1, 'upper-roman', '[IVX]{1,4}\.\ '), - 'I)' => array('[IVX]+\)\ ', 1, 'upper-roman'), // before A) ! - 'a)' => array('[a-z]\)\ ', 1, 'lower-alpha'), - 'A)' => array('[A-Z]\)\ ', 1, 'upper-alpha'), - ); - - - - public function __construct($texy) - { - $this->texy = $texy; - - $texy->addHandler('beforeParse', array($this, 'beforeParse')); - $texy->allowed['list'] = TRUE; - $texy->allowed['list/definition'] = TRUE; - } - - - - public function beforeParse() - { - $RE = $REul = array(); - foreach ($this->bullets as $desc) { - $RE[] = $desc[0]; - if (!$desc[1]) $REul[] = $desc[0]; - } - - $this->texy->registerBlockPattern( - array($this, 'patternList'), - '#^(?:'.TEXY_MODIFIER_H.'\n)?' // .{color: red} - . '('.implode('|', $RE).')\ *\S.*$#mUu', // item (unmatched) - 'list' - ); - - $this->texy->registerBlockPattern( - array($this, 'patternDefList'), - '#^(?:'.TEXY_MODIFIER_H.'\n)?' // .{color:red} - . '(\S.*)\:\ *'.TEXY_MODIFIER_H.'?\n' // Term: - . '(\ ++)('.implode('|', $REul).')\ *\S.*$#mUu', // - description - 'list/definition' - ); - } - - - - /** - * Callback for:. - * - * 1) .... .(title)[class]{style}> - * 2) .... - * + ... - * + ... - * 3) .... - * - * @param TexyBlockParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|FALSE - */ - public function patternList($parser, $matches) - { - list(, $mMod, $mBullet) = $matches; - // [1] => .(title)[class]{style}<> - // [2] => bullet * + - 1) a) A) IV) - - $tx = $this->texy; - - $el = TexyHtml::el(); - - $bullet = $min = NULL; - foreach ($this->bullets as $type => $desc) - if (preg_match('#'.$desc[0].'#Au', $mBullet)) { - $bullet = isset($desc[3]) ? $desc[3] : $desc[0]; - $min = isset($desc[3]) ? 2 : 1; - $el->setName($desc[1] ? 'ol' : 'ul'); - $el->attrs['style']['list-style-type'] = $desc[2]; - if ($desc[1]) { // ol - if ($type[0] === '1' && (int) $mBullet > 1) - $el->attrs['start'] = (int) $mBullet; - elseif ($type[0] === 'a' && $mBullet[0] > 'a') - $el->attrs['start'] = ord($mBullet[0]) - 96; - elseif ($type[0] === 'A' && $mBullet[0] > 'A') - $el->attrs['start'] = ord($mBullet[0]) - 64; - } - break; - } - - $mod = new TexyModifier($mMod); - $mod->decorate($tx, $el); - - $parser->moveBackward(1); - - while ($elItem = $this->patternItem($parser, $bullet, FALSE, 'li')) { - $el->add($elItem); - } - - if ($el->count() < $min) return FALSE; - - // event listener - $tx->invokeHandlers('afterList', array($parser, $el, $mod)); - - return $el; - } - - - - /** - * Callback for:. - * - * Term: .(title)[class]{style}> - * - description 1 - * - description 2 - * - description 3 - * - * @param TexyBlockParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml - */ - public function patternDefList($parser, $matches) - { - list(, $mMod, , , , $mBullet) = $matches; - // [1] => .(title)[class]{style}<> - // [2] => ... - // [3] => .(title)[class]{style}<> - // [4] => space - // [5] => - * + - - $tx = $this->texy; - - $bullet = NULL; - foreach ($this->bullets as $desc) - if (preg_match('#'.$desc[0].'#Au', $mBullet)) { - $bullet = isset($desc[3]) ? $desc[3] : $desc[0]; - break; - } - - $el = TexyHtml::el('dl'); - $mod = new TexyModifier($mMod); - $mod->decorate($tx, $el); - $parser->moveBackward(2); - - $patternTerm = '#^\n?(\S.*)\:\ *'.TEXY_MODIFIER_H.'?()$#mUA'; - - while (TRUE) { - if ($elItem = $this->patternItem($parser, $bullet, TRUE, 'dd')) { - $el->add($elItem); - continue; - } - - if ($parser->next($patternTerm, $matches)) { - list(, $mContent, $mMod) = $matches; - // [1] => ... - // [2] => .(title)[class]{style}<> - - $elItem = TexyHtml::el('dt'); - $modItem = new TexyModifier($mMod); - $modItem->decorate($tx, $elItem); - - $elItem->parseLine($tx, $mContent); - $el->add($elItem); - continue; - } - - break; - } - - // event listener - $tx->invokeHandlers('afterDefinitionList', array($parser, $el, $mod)); - - return $el; - } - - - - /** - * Callback for single list item. - * - * @param TexyBlockParser - * @param string bullet type - * @param string left space - * @param string html tag - * @return TexyHtml|FALSE - */ - public function patternItem($parser, $bullet, $indented, $tag) - { - $tx = $this->texy; - $spacesBase = $indented ? ('\ {1,}') : ''; - $patternItem = "#^\n?($spacesBase)$bullet\\ *(\\S.*)?".TEXY_MODIFIER_H."?()$#mAUu"; - - // first line with bullet - $matches = NULL; - if (!$parser->next($patternItem, $matches)) return FALSE; - - list(, $mIndent, $mContent, $mMod) = $matches; - // [1] => indent - // [2] => ... - // [3] => .(title)[class]{style}<> - - $elItem = TexyHtml::el($tag); - $mod = new TexyModifier($mMod); - $mod->decorate($tx, $elItem); - - // next lines - $spaces = ''; - $content = ' ' . $mContent; // trick - while ($parser->next('#^(\n*)'.$mIndent.'(\ {1,'.$spaces.'})(.*)()$#Am', $matches)) { - list(, $mBlank, $mSpaces, $mContent) = $matches; - // [1] => blank line? - // [2] => spaces - // [3] => ... - - if ($spaces === '') $spaces = strlen($mSpaces); - $content .= "\n" . $mBlank . $mContent; - } - - // parse content - $elItem->parseBlock($tx, $content, TRUE); - - if (isset($elItem[0]) && $elItem[0] instanceof TexyHtml) { - $elItem[0]->setName(NULL); - } - - return $elItem; - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyLongWordsModule.php b/apigen/libs/Texy/texy/modules/TexyLongWordsModule.php deleted file mode 100644 index 16c1336009c..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyLongWordsModule.php +++ /dev/null @@ -1,201 +0,0 @@ -texy = $texy; - - $this->consonants = array_flip($this->consonants); - $this->vowels = array_flip($this->vowels); - $this->before_r = array_flip($this->before_r); - $this->before_l = array_flip($this->before_l); - $this->before_h = array_flip($this->before_h); - $this->doubleVowels = array_flip($this->doubleVowels); - - $texy->registerPostLine(array($this, 'postLine'), 'longwords'); - } - - - - public function postLine($text) - { - return preg_replace_callback( - '#[^\ \n\t\x14\x15\x16\x{2013}\x{2014}\x{ad}-]{'.$this->wordLimit.',}#u', - array($this, 'pattern'), - $text - ); - } - - - - /** - * Callback for long words. - * (c) David Grudl - * @param array - * @return string - */ - private function pattern($matches) - { - list($mWord) = $matches; - // [0] => lllloooonnnnggggwwwoorrdddd - - $chars = array(); - preg_match_all( - '#['.TEXY_MARK.']+|.#u', - $mWord, - $chars - ); - - $chars = $chars[0]; - if (count($chars) < $this->wordLimit) return $mWord; - - $consonants = $this->consonants; - $vowels = $this->vowels; - $before_r = $this->before_r; - $before_l = $this->before_l; - $before_h = $this->before_h; - $doubleVowels = $this->doubleVowels; - - $s = array(); - $trans = array(); - - $s[] = ''; - $trans[] = -1; - foreach ($chars as $key => $char) { - if (ord($char{0}) < 32) continue; - $s[] = $char; - $trans[] = $key; - } - $s[] = ''; - $len = count($s) - 2; - - $positions = array(); - $a = 0; $last = 1; - - while (++$a < $len) { - $hyphen = self::DONT; // Do not hyphenate - do { - if ($s[$a] === "\xC2\xA0") { $a++; continue 2; } // here and after never - - if ($s[$a] === '.') { $hyphen = self::HERE; break; } - - if (isset($consonants[$s[$a]])) { // consonants - - if (isset($vowels[$s[$a+1]])) { - if (isset($vowels[$s[$a-1]])) $hyphen = self::HERE; - break; - } - - if (($s[$a] === 's') && ($s[$a-1] === 'n') && isset($consonants[$s[$a+1]])) { $hyphen = self::AFTER; break; } - - if (isset($consonants[$s[$a+1]]) && isset($vowels[$s[$a-1]])) { - if ($s[$a+1] === 'r') { - $hyphen = isset($before_r[$s[$a]]) ? self::HERE : self::AFTER; - break; - } - - if ($s[$a+1] === 'l') { - $hyphen = isset($before_l[$s[$a]]) ? self::HERE : self::AFTER; - break; - } - - if ($s[$a+1] === 'h') { // CH - $hyphen = isset($before_h[$s[$a]]) ? self::DONT : self::AFTER; - break; - } - - $hyphen = self::AFTER; - break; - } - - break; - } // end of consonants - - if (($s[$a] === 'u') && isset($doubleVowels[$s[$a-1]])) { $hyphen = self::AFTER; break; } - if (isset($vowels[$s[$a]]) && isset($vowels[$s[$a-1]])) { $hyphen = self::HERE; break; } - - } while(0); - - if ($hyphen === self::DONT && ($a - $last > $this->wordLimit*0.6)) $positions[] = $last = $a-1; // Hyphenate here - if ($hyphen === self::HERE) $positions[] = $last = $a-1; // Hyphenate here - if ($hyphen === self::AFTER) { $positions[] = $last = $a; $a++; } // Hyphenate after - - } // while - - - $a = end($positions); - if (($a === $len-1) && isset($consonants[$s[$len]])) - array_pop($positions); - - - $syllables = array(); - $last = 0; - foreach ($positions as $pos) { - if ($pos - $last > $this->wordLimit*0.6) { - $syllables[] = implode('', array_splice($chars, 0, $trans[$pos] - $trans[$last])); - $last = $pos; - } - } - $syllables[] = implode('', $chars); - - //$s = implode("\xC2\xAD", $syllables); // insert shy - //$s = str_replace(array("\xC2\xAD\xC2\xA0", "\xC2\xA0\xC2\xAD"), array(' ', ' '), $s); // shy+nbsp = normal space - - return implode("\xC2\xAD", $syllables);; - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyParagraphModule.php b/apigen/libs/Texy/texy/modules/TexyParagraphModule.php deleted file mode 100644 index 5d8644b30a2..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyParagraphModule.php +++ /dev/null @@ -1,136 +0,0 @@ -texy = $texy; - $texy->addHandler('paragraph', array($this, 'solve')); - } - - - - /** - * @param TexyBlockParser - * @param string text - * @param array - * @param TexyHtml - * @return vois - */ - public function process($parser, $content, $el) - { - $tx = $this->texy; - - if ($parser->isIndented()) { - $parts = preg_split('#(\n(?! )|\n{2,})#', $content, -1, PREG_SPLIT_NO_EMPTY); - } else { - $parts = preg_split('#(\n{2,})#', $content, -1, PREG_SPLIT_NO_EMPTY); - } - - foreach ($parts as $s) - { - $s = trim($s); - if ($s === '') continue; - - // try to find modifier - $mx = $mod = NULL; - if (preg_match('#\A(.*)(?<=\A|\S)'.TEXY_MODIFIER_H.'(\n.*)?()\z#sUm', $s, $mx)) { - list(, $mC1, $mMod, $mC2) = $mx; - $s = trim($mC1 . $mC2); - if ($s === '') continue; - $mod = new TexyModifier; - $mod->setProperties($mMod); - } - - $res = $tx->invokeAroundHandlers('paragraph', $parser, array($s, $mod)); - if ($res) $el->insert(NULL, $res); - } - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param string - * @param TexyModifier|NULL - * @return TexyHtml|FALSE - */ - public function solve($invocation, $content, $mod) - { - $tx = $this->texy; - - // find hard linebreaks - if ($tx->mergeLines) { - // .... - // ... => \r means break line - $content = preg_replace('#\n +(?=\S)#', "\r", $content); - } else { - $content = preg_replace('#\n#', "\r", $content); - } - - $el = TexyHtml::el('p'); - $el->parseLine($tx, $content); - $content = $el->getText(); // string - - // check content type - // block contains block tag - if (strpos($content, Texy::CONTENT_BLOCK) !== FALSE) { - $el->setName(NULL); // ignores modifier! - - // block contains text (protected) - } elseif (strpos($content, Texy::CONTENT_TEXTUAL) !== FALSE) { - // leave element p - - // block contains text - } elseif (preg_match('#[^\s'.TEXY_MARK.']#u', $content)) { - // leave element p - - // block contains only replaced element - } elseif (strpos($content, Texy::CONTENT_REPLACED) !== FALSE) { - $el->setName($tx->nontextParagraph); - - // block contains only markup tags or spaces or nothing - } else { - // if {ignoreEmptyStuff} return FALSE; - if (!$mod) $el->setName(NULL); - } - - if ($el->getName()) { - // apply modifier - if ($mod) $mod->decorate($tx, $el); - - // add
- if (strpos($content, "\r") !== FALSE) { - $key = $tx->protect('
', Texy::CONTENT_REPLACED); - $content = str_replace("\r", $key, $content); - }; - } - - $content = strtr($content, "\r\n", ' '); - $el->setText($content); - - return $el; - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyPhraseModule.php b/apigen/libs/Texy/texy/modules/TexyPhraseModule.php deleted file mode 100644 index 24a1a5dc8b5..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyPhraseModule.php +++ /dev/null @@ -1,334 +0,0 @@ - 'strong', // or 'b' - 'phrase/em' => 'em', // or 'i' - 'phrase/em-alt' => 'em', - 'phrase/em-alt2' => 'em', - 'phrase/ins' => 'ins', - 'phrase/del' => 'del', - 'phrase/sup' => 'sup', - 'phrase/sup-alt' => 'sup', - 'phrase/sub' => 'sub', - 'phrase/sub-alt' => 'sub', - 'phrase/span' => 'a', - 'phrase/span-alt' => 'a', - 'phrase/cite' => 'cite', - 'phrase/acronym' => 'acronym', - 'phrase/acronym-alt' => 'acronym', - 'phrase/code' => 'code', - 'phrase/quote' => 'q', - 'phrase/quicklink' => 'a', - ); - - - /** @var bool are links allowed? */ - public $linksAllowed = TRUE; - - - - public function __construct($texy) - { - $this->texy = $texy; - - $texy->addHandler('phrase', array($this, 'solve')); - -/* - // UNIVERSAL - $texy->registerLinePattern( - array($this, 'patternPhrase'), - '#((?>([*+/^_"~`-])+?))(?!\s)(.*(?!\\2).)'.TEXY_MODIFIER.'?(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?()"\''.TEXY_MARK.'-])\*(?![\s*])(.+)'.TEXY_MODIFIER.'?(?()"?!\'-])'.TEXY_LINK.'??()#Uus', - 'phrase/em-alt2' - ); - - // ++inserted++ - $texy->registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?-])([^\r\n]+)'.TEXY_MODIFIER.'?(?-])()#Uu', - 'phrase/del' - ); - - // ^^superscript^^ - $texy->registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternSupSub'), - '#(?<=[a-z0-9])\^([n0-9+-]{1,4}?)(?![a-z0-9])#Uui', - 'phrase/sup-alt' - ); - - // __subscript__ - $texy->registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternSupSub'), - '#(?<=[a-z])\_([n0-9]{1,3})(?![a-z0-9])#Uui', - 'phrase/sub-alt' - ); - - // "span" - $texy->registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?>quote<< - $texy->registerLinePattern( - array($this, 'patternPhrase'), - '#(?)\>\>(?![\s>])([^\r\n]+)'.TEXY_MODIFIER.'?(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#(?registerLinePattern( - array($this, 'patternNoTexy'), - '#(?registerLinePattern( - array($this, 'patternPhrase'), - '#\`(\S[^'.TEXY_MARK.'\r\n]*)'.TEXY_MODIFIER.'?(?registerLinePattern( - array($this, 'patternPhrase'), - '#(['.TEXY_CHAR.'0-9@\#$%&.,_-]+)()(?=:\[)'.TEXY_LINK.'()#Uu', - 'phrase/quicklink' - ); - - - $texy->allowed['phrase/ins'] = FALSE; - $texy->allowed['phrase/del'] = FALSE; - $texy->allowed['phrase/sup'] = FALSE; - $texy->allowed['phrase/sub'] = FALSE; - $texy->allowed['phrase/cite'] = FALSE; - } - - - - /** - * Callback for: **.... .(title)[class]{style}**:LINK. - * - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternPhrase($parser, $matches, $phrase) - { - list(, $mContent, $mMod, $mLink) = $matches; - // [1] => ** - // [2] => ... - // [3] => .(title)[class]{style} - // [4] => LINK - - $tx = $this->texy; - $mod = new TexyModifier($mMod); - $link = NULL; - - $parser->again = $phrase !== 'phrase/code' && $phrase !== 'phrase/quicklink'; - - if ($phrase === 'phrase/span' || $phrase === 'phrase/span-alt') { - if ($mLink == NULL) { - if (!$mMod) return FALSE; // means "..." - } else { - $link = $tx->linkModule->factoryLink($mLink, $mMod, $mContent); - } - - } elseif ($phrase === 'phrase/acronym' || $phrase === 'phrase/acronym-alt') { - $mod->title = trim(Texy::unescapeHtml($mLink)); - - } elseif ($phrase === 'phrase/quote') { - $mod->cite = $tx->blockQuoteModule->citeLink($mLink); - - } elseif ($mLink != NULL) { - $link = $tx->linkModule->factoryLink($mLink, NULL, $mContent); - } - - return $tx->invokeAroundHandlers('phrase', $parser, array($phrase, $mContent, $mod, $link)); - } - - - - /** - * Callback for: any^2 any_2. - * - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternSupSub($parser, $matches, $phrase) - { - list(, $mContent) = $matches; - $mod = new TexyModifier(); - $link = NULL; - $mContent = str_replace('-', "\xE2\x88\x92", $mContent); // − - return $this->texy->invokeAroundHandlers('phrase', $parser, array($phrase, $mContent, $mod, $link)); - } - - - - /** - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return string - */ - public function patternNoTexy($parser, $matches) - { - list(, $mContent) = $matches; - return $this->texy->protect(Texy::escapeHtml($mContent), Texy::CONTENT_TEXTUAL); - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param string - * @param string - * @param TexyModifier - * @param TexyLink - * @return TexyHtml - */ - public function solve($invocation, $phrase, $content, $mod, $link) - { - $tx = $this->texy; - - $tag = isset($this->tags[$phrase]) ? $this->tags[$phrase] : NULL; - - if ($tag === 'a') - $tag = $link && $this->linksAllowed ? NULL : 'span'; - - if ($phrase === 'phrase/code') - $content = $tx->protect(Texy::escapeHtml($content), Texy::CONTENT_TEXTUAL); - - if ($phrase === 'phrase/strong+em') { - $el = TexyHtml::el($this->tags['phrase/strong']); - $el->create($this->tags['phrase/em'], $content); - $mod->decorate($tx, $el); - - } elseif ($tag) { - $el = TexyHtml::el($tag)->setText($content); - $mod->decorate($tx, $el); - - if ($tag === 'q') $el->attrs['cite'] = $mod->cite; - } else { - $el = $content; // trick - } - - if ($link && $this->linksAllowed) return $tx->linkModule->solve(NULL, $link, $el); - - return $el; - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyScriptModule.php b/apigen/libs/Texy/texy/modules/TexyScriptModule.php deleted file mode 100644 index 8b1efa630dc..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyScriptModule.php +++ /dev/null @@ -1,119 +0,0 @@ -texy = $texy; - - $texy->addHandler('script', array($this, 'solve')); - - $texy->registerLinePattern( - array($this, 'pattern'), - '#\{\{([^'.TEXY_MARK.']+)\}\}()#U', - 'script' - ); - } - - - - /** - * Callback for: {{...}}. - * - * @param TexyLineParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function pattern($parser, $matches) - { - list(, $mContent) = $matches; - // [1] => ... - - $cmd = trim($mContent); - if ($cmd === '') return FALSE; - - $args = $raw = NULL; - // function(arg, arg, ...) or function: arg, arg - if (preg_match('#^([a-z_][a-z0-9_-]*)\s*(?:\(([^()]*)\)|:(.*))$#iu', $cmd, $matches)) { - $cmd = $matches[1]; - $raw = isset($matches[3]) ? trim($matches[3]) : trim($matches[2]); - if ($raw === '') - $args = array(); - else - $args = preg_split('#\s*' . preg_quote($this->separator, '#') . '\s*#u', $raw); - } - - // Texy 1.x way - if ($this->handler) { - if (is_callable(array($this->handler, $cmd))) { - array_unshift($args, $parser); - return call_user_func_array(array($this->handler, $cmd), $args); - } - - if (is_callable($this->handler)) - return call_user_func_array($this->handler, array($parser, $cmd, $args, $raw)); - } - - // Texy 2 way - return $this->texy->invokeAroundHandlers('script', $parser, array($cmd, $args, $raw)); - } - - - - /** - * Finish invocation. - * - * @param TexyHandlerInvocation handler invocation - * @param string command - * @param array arguments - * @param string arguments in raw format - * @return TexyHtml|string|FALSE - */ - public function solve($invocation, $cmd, $args, $raw) - { - if ($cmd === 'texy') { - if (!$args) return FALSE; - - switch ($args[0]) { - case 'nofollow': - $this->texy->linkModule->forceNoFollow = TRUE; - break; - } - return ''; - - } else { - return FALSE; - } - } - -} diff --git a/apigen/libs/Texy/texy/modules/TexyTableModule.php b/apigen/libs/Texy/texy/modules/TexyTableModule.php deleted file mode 100644 index 7279b9912e9..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyTableModule.php +++ /dev/null @@ -1,309 +0,0 @@ -texy = $texy; - - $texy->registerBlockPattern( - array($this, 'patternTable'), - '#^(?:'.TEXY_MODIFIER_HV.'\n)?' // .{color: red} - . '\|.*()$#mU', // | .... - 'table' - ); - } - - - - /** - * Callback for:. - * - * .(title)[class]{style}> - * |------------------ - * | xxx | xxx | xxx | .(..){..}[..] - * |------------------ - * | aa | bb | cc | - * - * @param TexyBlockParser - * @param array regexp matches - * @param string pattern name - * @return TexyHtml|string|FALSE - */ - public function patternTable($parser, $matches) - { - if ($this->disableTables) return FALSE; - list(, $mMod) = $matches; - // [1] => .(title)[class]{style}<>_ - - $tx = $this->texy; - - $el = TexyHtml::el('table'); - $mod = new TexyModifier($mMod); - $mod->decorate($tx, $el); - - $parser->moveBackward(); - - if ($parser->next('#^\|(\#|\=){2,}(?![|\#=+])(.+)\\1*\|? *'.TEXY_MODIFIER_H.'?()$#Um', $matches)) { - list(, , $mContent, $mMod) = $matches; - // [1] => # / = - // [2] => .... - // [3] => .(title)[class]{style}<> - - $caption = $el->create('caption'); - $mod = new TexyModifier($mMod); - $mod->decorate($tx, $caption); - $caption->parseLine($tx, $mContent); - } - - $isHead = FALSE; - $colModifier = array(); - $prevRow = array(); // rowSpan building helper - $rowCounter = 0; - $colCounter = 0; - $elPart = NULL; - $lineMode = FALSE; // rows must be separated by lines - - while (TRUE) { - if ($parser->next('#^\|([=-])[+|=-]{2,}$#Um', $matches)) { // line - if ($lineMode) { - if ($matches[1] === '=') $isHead = !$isHead; - } else { - $isHead = !$isHead; - $lineMode = $matches[1] === '='; - } - $prevRow = array(); - continue; - } - - if ($parser->next('#^\|(.*)(?:|\|\ *'.TEXY_MODIFIER_HV.'?)()$#U', $matches)) { - // smarter head detection - if ($rowCounter === 0 && !$isHead && $parser->next('#^\|[=-][+|=-]{2,}$#Um', $foo)) { - $isHead = TRUE; - $parser->moveBackward(); - } - - if ($elPart === NULL) { - $elPart = $el->create($isHead ? 'thead' : 'tbody'); - - } elseif (!$isHead && $elPart->getName() === 'thead') { - $this->finishPart($elPart); - $elPart = $el->create('tbody'); - } - - - // PARSE ROW - list(, $mContent, $mMod) = $matches; - // [1] => .... - // [2] => .(title)[class]{style}<>_ - - $elRow = TexyHtml::el('tr'); - $mod = new TexyModifier($mMod); - $mod->decorate($tx, $elRow); - - $rowClass = $rowCounter % 2 === 0 ? $this->oddClass : $this->evenClass; - if ($rowClass && !isset($mod->classes[$this->oddClass]) && !isset($mod->classes[$this->evenClass])) { - $elRow->attrs['class'][] = $rowClass; - } - - $col = 0; - $elCell = NULL; - - // special escape sequence \| - $mContent = str_replace('\\|', "\x13", $mContent); - $mContent = preg_replace('#(\[[^\]]*)\|#', "$1\x13", $mContent); // HACK: support for [..|..] - - foreach (explode('|', $mContent) as $cell) { - $cell = strtr($cell, "\x13", '|'); - // rowSpan - if (isset($prevRow[$col]) && ($lineMode || preg_match('#\^\ *$|\*??(.*)\ +\^$#AU', $cell, $matches))) { - $prevRow[$col]->rowSpan++; - if (!$lineMode) { - $cell = isset($matches[1]) ? $matches[1] : ''; - } - $prevRow[$col]->text .= "\n" . $cell; - $col += $prevRow[$col]->colSpan; - $elCell = NULL; - continue; - } - - // colSpan - if ($cell === '' && $elCell) { - $elCell->colSpan++; - unset($prevRow[$col]); - $col++; - continue; - } - - // common cell - if (!preg_match('#(\*??)\ *'.TEXY_MODIFIER_HV.'??(.*)'.TEXY_MODIFIER_HV.'?\ *()$#AU', $cell, $matches)) continue; - list(, $mHead, $mModCol, $mContent, $mMod) = $matches; - // [1] => * ^ - // [2] => .(title)[class]{style}<>_ - // [3] => .... - // [4] => .(title)[class]{style}<>_ - - if ($mModCol) { - $colModifier[$col] = new TexyModifier($mModCol); - } - - if (isset($colModifier[$col])) - $mod = clone $colModifier[$col]; - else - $mod = new TexyModifier; - - $mod->setProperties($mMod); - - $elCell = new TexyTableCellElement; - $elCell->setName($isHead || ($mHead === '*') ? 'th' : 'td'); - $mod->decorate($tx, $elCell); - $elCell->text = $mContent; - - $elRow->add($elCell); - $prevRow[$col] = $elCell; - $col++; - } - - - // even up with empty cells - while ($col < $colCounter) { - if (isset($prevRow[$col]) && $lineMode) { - $prevRow[$col]->rowSpan++; - $prevRow[$col]->text .= "\n"; - - } else { - $elCell = new TexyTableCellElement; - $elCell->setName($isHead ? 'th' : 'td'); - if (isset($colModifier[$col])) { - $colModifier[$col]->decorate($tx, $elCell); - } - $elRow->add($elCell); - $prevRow[$col] = $elCell; - } - $col++; - } - $colCounter = $col; - - - if ($elRow->count()) { - $elPart->add($elRow); - $rowCounter++; - } else { - // redundant row - foreach ($prevRow as $elCell) $elCell->rowSpan--; - } - - continue; - } - - break; - } - - if ($elPart === NULL) { - // invalid table - return FALSE; - } - - if ($elPart->getName() === 'thead') { - // thead is optional, tbody is required - $elPart->setName('tbody'); - } - - $this->finishPart($elPart); - - - // event listener - $tx->invokeHandlers('afterTable', array($parser, $el, $mod)); - - return $el; - } - - - - /** - * Parse text in all cells. - * @param TexyHtml - * @return void - */ - private function finishPart($elPart) - { - $tx = $this->texy; - - foreach ($elPart->getChildren() as $elRow) - { - foreach ($elRow->getChildren() as $elCell) - { - if ($elCell->colSpan > 1) { - $elCell->attrs['colspan'] = $elCell->colSpan; - } - - if ($elCell->rowSpan > 1) { - $elCell->attrs['rowspan'] = $elCell->rowSpan; - } - - $text = rtrim($elCell->text); - if (strpos($text, "\n") !== FALSE) { - // multiline parse as block - // HACK: disable tables - $this->disableTables = TRUE; - $elCell->parseBlock($tx, Texy::outdent($text)); - $this->disableTables = FALSE; - } else { - $elCell->parseLine($tx, ltrim($text)); - } - - if ($elCell->getText() === '') { - $elCell->setText("\xC2\xA0"); //   - } - } - } - } - -} - - - - -/** - * Table cell TD / TH. - * @package Texy - */ -class TexyTableCellElement extends TexyHtml -{ - /** @var int */ - public $colSpan = 1; - - /** @var int */ - public $rowSpan = 1; - - /** @var string */ - public $text; - -} diff --git a/apigen/libs/Texy/texy/modules/TexyTypographyModule.php b/apigen/libs/Texy/texy/modules/TexyTypographyModule.php deleted file mode 100644 index e4ad662fa85..00000000000 --- a/apigen/libs/Texy/texy/modules/TexyTypographyModule.php +++ /dev/null @@ -1,139 +0,0 @@ - array( - 'singleQuotes' => array("\xe2\x80\x9a", "\xe2\x80\x98"), // U+201A, U+2018 - 'doubleQuotes' => array("\xe2\x80\x9e", "\xe2\x80\x9c"), // U+201E, U+201C - ), - - 'en' => array( - 'singleQuotes' => array("\xe2\x80\x98", "\xe2\x80\x99"), // U+2018, U+2019 - 'doubleQuotes' => array("\xe2\x80\x9c", "\xe2\x80\x9d"), // U+201C, U+201D - ), - - 'fr' => array( - 'singleQuotes' => array("\xe2\x80\xb9", "\xe2\x80\xba"), // U+2039, U+203A - 'doubleQuotes' => array("\xc2\xab", "\xc2\xbb"), // U+00AB, U+00BB - ), - - 'de' => array( - 'singleQuotes' => array("\xe2\x80\x9a", "\xe2\x80\x98"), // U+201A, U+2018 - 'doubleQuotes' => array("\xe2\x80\x9e", "\xe2\x80\x9c"), // U+201E, U+201C - ), - - 'pl' => array( - 'singleQuotes' => array("\xe2\x80\x9a", "\xe2\x80\x99"), // U+201A, U+2019 - 'doubleQuotes' => array("\xe2\x80\x9e", "\xe2\x80\x9d"), // U+201E, U+201D - ), - ); - - /** @var string */ - public $locale = 'cs'; - - /** @var array */ - private $pattern, $replace; - - - - public function __construct($texy) - { - $this->texy = $texy; - $texy->registerPostLine(array($this, 'postLine'), 'typography'); - $texy->addHandler('beforeParse', array($this, 'beforeParse')); - } - - - - /** - * Text pre-processing. - * @param Texy - * @param string - * @return void - */ - public function beforeParse($texy, & $text) - { - // CONTENT_MARKUP mark: \x17-\x1F - // CONTENT_REPLACED mark: \x16 - // CONTENT_TEXTUAL mark: \x17 - // CONTENT_BLOCK: not used in postLine - - if (isset(self::$locales[$this->locale])) - $locale = self::$locales[$this->locale]; - else // fall back - $locale = self::$locales['en']; - - $pairs = array( - '#(? "\xe2\x80\xa6", // ellipsis ... - '#(?<=[\d ]|^)-(?=[\d ]|$)#' => "\xe2\x80\x93", // en dash 123-123 - '#(?<=[^!*+,/:;<=>@\\\\_|-])--(?=[^!*+,/:;<=>@\\\\_|-])#' => "\xe2\x80\x93", // en dash alphanum--alphanum - '#,-#' => ",\xe2\x80\x93", // en dash ,- - '#(? "\$1\xc2\xa0\$2\xc2\xa0\$3", // date 23. 1. 1978 - '#(? "\$1\xc2\xa0\$2", // date 23. 1. - '# --- #' => "\xc2\xa0\xe2\x80\x94 ", // em dash --- - '# ([\x{2013}\x{2014}])#u' => "\xc2\xa0\$1", //   behind dash (dash stays at line end) - '# <-{1,2}> #' => " \xe2\x86\x94 ", // left right arrow <--> - '#-{1,}> #' => " \xe2\x86\x92 ", // right arrow --> - '# <-{1,}#' => " \xe2\x86\x90 ", // left arrow <-- - '#={1,}> #' => " \xe2\x87\x92 ", // right arrow ==> - '#\\+-#' => "\xc2\xb1", // +- - '#(\d+)( ?)x\\2(?=\d)#' => "\$1\xc3\x97", // dimension sign 123 x 123... - '#(?<=\d)x(?= |,|.|$)#m' => "\xc3\x97", // dimension sign 123x - '#(\S ?)\(TM\)#i' => "\$1\xe2\x84\xa2", // trademark (TM) - '#(\S ?)\(R\)#i' => "\$1\xc2\xae", // registered (R) - '#\(C\)( ?\S)#i' => "\xc2\xa9\$1", // copyright (C) - '#\(EUR\)#' => "\xe2\x82\xac", // Euro (EUR) - '#(\d) (?=\d{3})#' => "\$1\xc2\xa0", // (phone) number 1 123 123 123... - - '#(?<=[^\s\x17])\s+([\x17-\x1F]+)(?=\s)#u'=> "\$1", // remove intermarkup space phase 1 - '#(?<=\s)([\x17-\x1F]+)\s+#u' => "\$1", // remove intermarkup space phase 2 - - '#(?<=.{50})\s+(?=[\x17-\x1F]*\S{1,6}[\x17-\x1F]*$)#us' => "\xc2\xa0", // space before last short word - - // nbsp space between number (optionally followed by dot) and word, symbol, punctation, currency symbol - '#(?<=^| |\.|,|-|\+|\x16|\(|\d\x{A0})([\x17-\x1F]*\d+\.?[\x17-\x1F]*)\s+(?=[\x17-\x1F]*[%'.TEXY_CHAR.'\x{b0}-\x{be}\x{2020}-\x{214f}])#mu' - => "\$1\xc2\xa0", - - // space between preposition and word - '#(?<=^|[^0-9'.TEXY_CHAR.'])([\x17-\x1F]*[ksvzouiKSVZOUIA][\x17-\x1F]*)\s+(?=[\x17-\x1F]*[0-9'.TEXY_CHAR.'])#mus' - => "\$1\xc2\xa0", - - '#(? $locale['doubleQuotes'][0].'$1'.$locale['doubleQuotes'][1], // double "" - '#(? $locale['singleQuotes'][0].'$1'.$locale['singleQuotes'][1], // single '' - ); - - $this->pattern = array_keys($pairs); - $this->replace = array_values($pairs); - } - - - - public function postLine($text, $preserveSpaces = FALSE) - { - if (!$preserveSpaces) { - $text = preg_replace('# {2,}#', ' ', $text); - } - return preg_replace($this->pattern, $this->replace, $text); - } - -} \ No newline at end of file diff --git a/apigen/libs/Texy/texy/netterobots.txt b/apigen/libs/Texy/texy/netterobots.txt deleted file mode 100644 index 45b983866bc..00000000000 --- a/apigen/libs/Texy/texy/netterobots.txt +++ /dev/null @@ -1 +0,0 @@ -Disallow: /modules diff --git a/apigen/libs/Texy/texy/texy.php b/apigen/libs/Texy/texy/texy.php deleted file mode 100644 index 58ff07e01bb..00000000000 --- a/apigen/libs/Texy/texy/texy.php +++ /dev/null @@ -1,965 +0,0 @@ - - * $texy = new Texy(); - * $html = $texy->process($text); - *
- * - * @copyright Copyright (c) 2004, 2010 David Grudl - * @package Texy - */ -class Texy extends TexyObject -{ - // configuration directives - const ALL = TRUE; - const NONE = FALSE; - - // Texy version - const VERSION = TEXY_VERSION; - const REVISION = '$WCREV$ released on $WCDATE$'; - - // types of protection marks - const CONTENT_MARKUP = "\x17"; - const CONTENT_REPLACED = "\x16"; - const CONTENT_TEXTUAL = "\x15"; - const CONTENT_BLOCK = "\x14"; - - // url filters - const FILTER_ANCHOR = 'anchor'; - const FILTER_IMAGE = 'image'; - - // HTML minor-modes - const XML = 2; - - // HTML modes - const HTML4_TRANSITIONAL = 0; - const HTML4_STRICT = 1; - const HTML5 = 4; - const XHTML1_TRANSITIONAL = 2; // Texy::HTML4_TRANSITIONAL | Texy::XML; - const XHTML1_STRICT = 3; // Texy::HTML4_STRICT | Texy::XML; - const XHTML5 = 6; // Texy::HTML5 | Texy::XML; - - /** @var string input & output text encoding */ - public $encoding = 'utf-8'; - - /** @var array Texy! syntax configuration */ - public $allowed = array(); - - /** @var TRUE|FALSE|array Allowed HTML tags */ - public $allowedTags; - - /** @var TRUE|FALSE|array Allowed classes */ - public $allowedClasses = Texy::ALL; // all classes and id are allowed - - /** @var TRUE|FALSE|array Allowed inline CSS style */ - public $allowedStyles = Texy::ALL; // all inline styles are allowed - - /** @var int TAB width (for converting tabs to spaces) */ - public $tabWidth = 8; - - /** @var boolean Do obfuscate e-mail addresses? */ - public $obfuscateEmail = TRUE; - - /** @var array regexps to check URL schemes */ - public $urlSchemeFilters = NULL; // disable URL scheme filter - - /** @var bool Paragraph merging mode */ - public $mergeLines = TRUE; - - /** @var array Parsing summary */ - public $summary = array( - 'images' => array(), - 'links' => array(), - 'preload' => array(), - ); - - /** @var string Generated stylesheet */ - public $styleSheet = ''; - - /** @var array CSS classes for align modifiers */ - public $alignClasses = array( - 'left' => NULL, - 'right' => NULL, - 'center' => NULL, - 'justify' => NULL, - 'top' => NULL, - 'middle' => NULL, - 'bottom' => NULL, - ); - - /** @var bool remove soft hyphens (SHY)? */ - public $removeSoftHyphens = TRUE; - - /** @var mixed */ - public static $advertisingNotice = 'once'; - - /** @var string */ - public $nontextParagraph = 'div'; - - /** @var TexyScriptModule */ - public $scriptModule; - - /** @var TexyParagraphModule */ - public $paragraphModule; - - /** @var TexyHtmlModule */ - public $htmlModule; - - /** @var TexyImageModule */ - public $imageModule; - - /** @var TexyLinkModule */ - public $linkModule; - - /** @var TexyPhraseModule */ - public $phraseModule; - - /** @var TexyEmoticonModule */ - public $emoticonModule; - - /** @var TexyBlockModule */ - public $blockModule; - - /** @var TexyHeadingModule */ - public $headingModule; - - /** @var TexyHorizLineModule */ - public $horizLineModule; - - /** @var TexyBlockQuoteModule */ - public $blockQuoteModule; - - /** @var TexyListModule */ - public $listModule; - - /** @var TexyTableModule */ - public $tableModule; - - /** @var TexyFigureModule */ - public $figureModule; - - /** @var TexyTypographyModule */ - public $typographyModule; - - /** @var TexyLongWordsModule */ - public $longWordsModule; - - /** @var TexyHtmlOutputModule */ - public $htmlOutputModule; - - - /** - * Registered regexps and associated handlers for inline parsing. - * @var array of ('handler' => callback - * 'pattern' => regular expression) - */ - private $linePatterns = array(); - private $_linePatterns; - - /** - * Registered regexps and associated handlers for block parsing. - * @var array of ('handler' => callback - * 'pattern' => regular expression) - */ - private $blockPatterns = array(); - private $_blockPatterns; - - /** @var array */ - private $postHandlers = array(); - - /** @var TexyHtml DOM structure for parsed text */ - private $DOM; - - /** @var array Texy protect markup table */ - private $marks = array(); - - /** @var array for internal usage */ - public $_classes, $_styles; - - /** @var bool */ - private $processing; - - /** @var array of events and registered handlers */ - private $handlers = array(); - - /** - * DTD descriptor. - * $dtd[element][0] - allowed attributes (as array keys) - * $dtd[element][1] - allowed content for an element (content model) (as array keys) - * - array of allowed elements (as keys) - * - FALSE - empty element - * - 0 - special case for ins & del - * @var array - */ - public $dtd; - - /** @var array */ - private static $dtdCache; - - /** @var int HTML mode */ - private $mode; - - - /** DEPRECATED */ - public static $strictDTD; - public $cleaner; - public $xhtml; - - - - public function __construct() - { - // load all modules - $this->loadModules(); - - // DEPRECATED - if (self::$strictDTD !== NULL) { - $this->setOutputMode(self::$strictDTD ? self::XHTML1_STRICT : self::XHTML1_TRANSITIONAL); - } else { - $this->setOutputMode(self::XHTML1_TRANSITIONAL); - } - - // DEPRECATED - $this->cleaner = & $this->htmlOutputModule; - - // examples of link references ;-) - $link = new TexyLink('http://texy.info/'); - $link->modifier->title = 'The best text -> HTML converter and formatter'; - $link->label = 'Texy!'; - $this->linkModule->addReference('texy', $link); - - $link = new TexyLink('http://www.google.com/search?q=%s'); - $this->linkModule->addReference('google', $link); - - $link = new TexyLink('http://en.wikipedia.org/wiki/Special:Search?search=%s'); - $this->linkModule->addReference('wikipedia', $link); - } - - - - /** - * Set HTML/XHTML output mode (overwrites self::$allowedTags) - * @param int - * @return void - */ - public function setOutputMode($mode) - { - if (!in_array($mode, array(self::HTML4_TRANSITIONAL, self::HTML4_STRICT, - self::HTML5, self::XHTML1_TRANSITIONAL, self::XHTML1_STRICT, self::XHTML5), TRUE)) { - throw new InvalidArgumentException("Invalid mode."); - } - - if (!isset(self::$dtdCache[$mode])) { - require dirname(__FILE__) . '/libs/DTD.php'; - self::$dtdCache[$mode] = $dtd; - } - - $this->mode = $mode; - $this->dtd = self::$dtdCache[$mode]; - TexyHtml::$xhtml = (bool) ($mode & self::XML); // TODO: remove? - - // accept all valid HTML tags and attributes by default - $this->allowedTags = array(); - foreach ($this->dtd as $tag => $dtd) { - $this->allowedTags[$tag] = self::ALL; - } - } - - - - /** - * Get HTML/XHTML output mode - * @return int - */ - public function getOutputMode() - { - return $this->mode; - } - - - - /** - * Create array of all used modules ($this->modules). - * This array can be changed by overriding this method (by subclasses) - */ - protected function loadModules() - { - // line parsing - $this->scriptModule = new TexyScriptModule($this); - $this->htmlModule = new TexyHtmlModule($this); - $this->imageModule = new TexyImageModule($this); - $this->phraseModule = new TexyPhraseModule($this); - $this->linkModule = new TexyLinkModule($this); - $this->emoticonModule = new TexyEmoticonModule($this); - - // block parsing - $this->paragraphModule = new TexyParagraphModule($this); - $this->blockModule = new TexyBlockModule($this); - $this->figureModule = new TexyFigureModule($this); - $this->horizLineModule = new TexyHorizLineModule($this); - $this->blockQuoteModule = new TexyBlockQuoteModule($this); - $this->tableModule = new TexyTableModule($this); - $this->headingModule = new TexyHeadingModule($this); - $this->listModule = new TexyListModule($this); - - // post process - $this->typographyModule = new TexyTypographyModule($this); - $this->longWordsModule = new TexyLongWordsModule($this); - $this->htmlOutputModule = new TexyHtmlOutputModule($this); - } - - - - final public function registerLinePattern($handler, $pattern, $name, $againTest = NULL) - { - if (!is_callable($handler)) { - $able = is_callable($handler, TRUE, $textual); - throw new InvalidArgumentException("Handler '$textual' is not " . ($able ? 'callable.' : 'valid PHP callback.')); - } - - if (!isset($this->allowed[$name])) $this->allowed[$name] = TRUE; - - $this->linePatterns[$name] = array( - 'handler' => $handler, - 'pattern' => $pattern, - 'again' => $againTest, - ); - } - - - - final public function registerBlockPattern($handler, $pattern, $name) - { - if (!is_callable($handler)) { - $able = is_callable($handler, TRUE, $textual); - throw new InvalidArgumentException("Handler '$textual' is not " . ($able ? 'callable.' : 'valid PHP callback.')); - } - - // if (!preg_match('#(.)\^.*\$\\1[a-z]*#is', $pattern)) die("Texy: Not a block pattern $name"); - if (!isset($this->allowed[$name])) $this->allowed[$name] = TRUE; - - $this->blockPatterns[$name] = array( - 'handler' => $handler, - 'pattern' => $pattern . 'm', // force multiline - ); - } - - - - final public function registerPostLine($handler, $name) - { - if (!is_callable($handler)) { - $able = is_callable($handler, TRUE, $textual); - throw new InvalidArgumentException("Handler '$textual' is not " . ($able ? 'callable.' : 'valid PHP callback.')); - } - - if (!isset($this->allowed[$name])) $this->allowed[$name] = TRUE; - - $this->postHandlers[$name] = $handler; - } - - - - /** - * Converts document in Texy! to (X)HTML code. - * - * @param string input text - * @param bool is single line? - * @return string output HTML code - */ - public function process($text, $singleLine = FALSE) - { - if ($this->processing) { - throw new InvalidStateException('Processing is in progress yet.'); - } - - // initialization - $this->marks = array(); - $this->processing = TRUE; - - // speed-up - if (is_array($this->allowedClasses)) $this->_classes = array_flip($this->allowedClasses); - else $this->_classes = $this->allowedClasses; - - if (is_array($this->allowedStyles)) $this->_styles = array_flip($this->allowedStyles); - else $this->_styles = $this->allowedStyles; - - // convert to UTF-8 (and check source encoding) - $text = TexyUtf::toUtf($text, $this->encoding); - - if ($this->removeSoftHyphens) { - $text = str_replace("\xC2\xAD", '', $text); - } - - // standardize line endings and spaces - $text = self::normalize($text); - - // replace tabs with spaces - $this->tabWidth = max(1, (int) $this->tabWidth); - while (strpos($text, "\t") !== FALSE) { - $text = preg_replace_callback('#^(.*)\t#mU', array($this, 'tabCb'), $text); - } - - // user before handler - $this->invokeHandlers('beforeParse', array($this, & $text, $singleLine)); - - // select patterns - $this->_linePatterns = $this->linePatterns; - $this->_blockPatterns = $this->blockPatterns; - foreach ($this->_linePatterns as $name => $foo) { - if (empty($this->allowed[$name])) unset($this->_linePatterns[$name]); - } - foreach ($this->_blockPatterns as $name => $foo) { - if (empty($this->allowed[$name])) unset($this->_blockPatterns[$name]); - } - - // parse Texy! document into internal DOM structure - $this->DOM = TexyHtml::el(); - if ($singleLine) { - $this->DOM->parseLine($this, $text); - } else { - $this->DOM->parseBlock($this, $text); - } - - // user after handler - $this->invokeHandlers('afterParse', array($this, $this->DOM, $singleLine)); - - // converts internal DOM structure to final HTML code - $html = $this->DOM->toHtml($this); - - // this notice should remain - if (self::$advertisingNotice) { - $html .= "\n"; - if (self::$advertisingNotice === 'once') { - self::$advertisingNotice = FALSE; - } - } - - $this->processing = FALSE; - - return TexyUtf::utf2html($html, $this->encoding); - } - - - - /** - * Converts single line in Texy! to (X)HTML code. - * - * @param string input text - * @return string output HTML code - */ - public function processLine($text) - { - return $this->process($text, TRUE); - } - - - - /** - * Makes only typographic corrections. - * @param string input text (in encoding defined by Texy::$encoding) - * @return string output text (in UTF-8) - */ - public function processTypo($text) - { - // convert to UTF-8 (and check source encoding) - $text = TexyUtf::toUtf($text, $this->encoding); - - // standardize line endings and spaces - $text = self::normalize($text); - - $this->typographyModule->beforeParse($this, $text); - $text = $this->typographyModule->postLine($text, TRUE); - - if (!empty($this->allowed['longwords'])) { - $text = $this->longWordsModule->postLine($text); - } - - return TexyUtf::utf2html($text, $this->encoding); - } - - - - /** - * Converts DOM structure to pure text. - * @return string - */ - public function toText() - { - if (!$this->DOM) { - throw new InvalidStateException('Call $texy->process() first.'); - } - - return TexyUtf::utfTo($this->DOM->toText($this), $this->encoding); - } - - - - /** - * Converts internal string representation to final HTML code in UTF-8. - * @return string - */ - final public function stringToHtml($s) - { - // decode HTML entities to UTF-8 - $s = self::unescapeHtml($s); - - // line-postprocessing - $blocks = explode(self::CONTENT_BLOCK, $s); - foreach ($this->postHandlers as $name => $handler) { - if (empty($this->allowed[$name])) continue; - foreach ($blocks as $n => $s) { - if ($n % 2 === 0 && $s !== '') { - $blocks[$n] = call_user_func($handler, $s); - } - } - } - $s = implode(self::CONTENT_BLOCK, $blocks); - - // encode < > & - $s = self::escapeHtml($s); - - // replace protected marks - $s = $this->unProtect($s); - - // wellform and reformat HTML - $this->invokeHandlers('postProcess', array($this, & $s)); - - // unfreeze spaces - $s = self::unfreezeSpaces($s); - - return $s; - } - - - - /** - * Converts internal string representation to final HTML code in UTF-8. - * @return string - */ - final public function stringToText($s) - { - $save = $this->htmlOutputModule->lineWrap; - $this->htmlOutputModule->lineWrap = FALSE; - $s = $this->stringToHtml( $s ); - $this->htmlOutputModule->lineWrap = $save; - - // remove tags - $s = preg_replace('#<(script|style)(.*)#Uis', '', $s); - $s = strip_tags($s); - $s = preg_replace('#\n\s*\n\s*\n[\n\s]*\n#', "\n\n", $s); - - // entities -> chars - $s = self::unescapeHtml($s); - - // convert nbsp to normal space and remove shy - $s = strtr($s, array( - "\xC2\xAD" => '', // shy - "\xC2\xA0" => ' ', // nbsp - )); - - return $s; - } - - - - /** - * Add new event handler. - * - * @param string event name - * @param callback - * @return void - */ - final public function addHandler($event, $callback) - { - if (!is_callable($callback)) { - $able = is_callable($callback, TRUE, $textual); - throw new InvalidArgumentException("Handler '$textual' is not " . ($able ? 'callable.' : 'valid PHP callback.')); - } - - $this->handlers[$event][] = $callback; - } - - - - /** - * Invoke registered around-handlers. - * - * @param string event name - * @param TexyParser actual parser object - * @param array arguments passed into handler - * @return mixed - */ - final public function invokeAroundHandlers($event, $parser, $args) - { - if (!isset($this->handlers[$event])) return FALSE; - - $invocation = new TexyHandlerInvocation($this->handlers[$event], $parser, $args); - $res = $invocation->proceed(); - $invocation->free(); - return $res; - } - - - - /** - * Invoke registered after-handlers. - * - * @param string event name - * @param array arguments passed into handler - * @return void - */ - final public function invokeHandlers($event, $args) - { - if (!isset($this->handlers[$event])) return; - - foreach ($this->handlers[$event] as $handler) { - call_user_func_array($handler, $args); - } - } - - - - /** - * Translate all white spaces (\t \n \r space) to meta-spaces \x01-\x04. - * which are ignored by TexyHtmlOutputModule routine - * @param string - * @return string - */ - final public static function freezeSpaces($s) - { - return strtr($s, " \t\r\n", "\x01\x02\x03\x04"); - } - - - - /** - * Reverts meta-spaces back to normal spaces. - * @param string - * @return string - */ - final public static function unfreezeSpaces($s) - { - return strtr($s, "\x01\x02\x03\x04", " \t\r\n"); - } - - - - /** - * Removes special controls characters and normalizes line endings and spaces. - * @param string - * @return string - */ - final public static function normalize($s) - { - // standardize line endings to unix-like - $s = str_replace("\r\n", "\n", $s); // DOS - $s = strtr($s, "\r", "\n"); // Mac - - // remove special chars; leave \t + \n - $s = preg_replace('#[\x00-\x08\x0B-\x1F]+#', '', $s); - - // right trim - $s = preg_replace("#[\t ]+$#m", '', $s); - - // trailing spaces - $s = trim($s, "\n"); - - return $s; - } - - - - /** - * Converts to web safe characters [a-z0-9-] text. - * @param string - * @param string - * @return string - */ - final public static function webalize($s, $charlist = NULL) - { - $s = TexyUtf::utf2ascii($s); - $s = strtolower($s); - $s = preg_replace('#[^a-z0-9'.preg_quote($charlist, '#').']+#', '-', $s); - $s = trim($s, '-'); - return $s; - } - - - - /** - * Texy! version of htmlSpecialChars (much faster than htmlSpecialChars!). - * note: " is not encoded! - * @param string - * @return string - */ - final public static function escapeHtml($s) - { - return str_replace(array('&', '<', '>'), array('&', '<', '>'), $s); - } - - - - /** - * Texy! version of html_entity_decode (always UTF-8, much faster than original!). - * @param string - * @return string - */ - final public static function unescapeHtml($s) - { - if (strpos($s, '&') === FALSE) return $s; - return html_entity_decode($s, ENT_QUOTES, 'UTF-8'); - } - - - - /** - * Outdents text block. - * @param string - * @return string - */ - final public static function outdent($s) - { - $s = trim($s, "\n"); - $spaces = strspn($s, ' '); - if ($spaces) return preg_replace("#^ {1,$spaces}#m", '', $s); - return $s; - } - - - - /** - * Generate unique mark - useful for freezing (folding) some substrings. - * @param string any string to froze - * @param int Texy::CONTENT_* constant - * @return string internal mark - */ - final public function protect($child, $contentType) - { - if ($child==='') return ''; - - $key = $contentType - . strtr(base_convert(count($this->marks), 10, 8), '01234567', "\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F") - . $contentType; - - $this->marks[$key] = $child; - - return $key; - } - - - - final public function unProtect($html) - { - return strtr($html, $this->marks); - } - - - - /** - * Filters bad URLs. - * @param string user URL - * @param string type: a-anchor, i-image, c-cite - * @return bool - */ - final public function checkURL($URL, $type) - { - // absolute URL with scheme? check scheme! - if (!empty($this->urlSchemeFilters[$type]) - && preg_match('#'.TEXY_URLSCHEME.'#A', $URL) - && !preg_match($this->urlSchemeFilters[$type], $URL)) - return FALSE; - - return TRUE; - } - - - - /** - * Is given URL relative? - * @param string URL - * @return bool - */ - final public static function isRelative($URL) - { - // check for scheme, or absolute path, or absolute URL - return !preg_match('#'.TEXY_URLSCHEME.'|[\#/?]#A', $URL); - } - - - - /** - * Prepends root to URL, if possible. - * @param string URL - * @param string root - * @return string - */ - final public static function prependRoot($URL, $root) - { - if ($root == NULL || !self::isRelative($URL)) return $URL; - return rtrim($root, '/\\') . '/' . $URL; - } - - - - final public function getLinePatterns() - { - return $this->_linePatterns; - } - - - - final public function getBlockPatterns() - { - return $this->_blockPatterns; - } - - - - final public function getDOM() - { - return $this->DOM; - } - - - - private function tabCb($m) - { - return $m[1] . str_repeat(' ', $this->tabWidth - strlen($m[1]) % $this->tabWidth); - } - - - - /** - * PHP garbage collector helper. - */ - final public function free() - { - if (version_compare(PHP_VERSION , '5.3', '<')) { - foreach (array_keys(get_object_vars($this)) as $key) { - $this->$key = NULL; - } - } - } - - - - final public function __clone() - { - throw new NotSupportedException('Clone is not supported.'); - } - -} diff --git a/apigen/libs/TokenReflection/LICENSE b/apigen/libs/TokenReflection/LICENSE deleted file mode 100644 index f9030f8e0a5..00000000000 --- a/apigen/libs/TokenReflection/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -PHP Token Reflection -Copyright (c) 2011-2012, Ondřej Nešpor, Jaroslav Hanslík. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * The names of authors and contributors may not be used to endorse or - promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/apigen/libs/TokenReflection/TokenReflection/Broker.php b/apigen/libs/TokenReflection/TokenReflection/Broker.php deleted file mode 100644 index 0276e3fadde..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Broker.php +++ /dev/null @@ -1,542 +0,0 @@ -cache = array( - self::CACHE_NAMESPACE => array(), - self::CACHE_CLASS => array(), - self::CACHE_CONSTANT => array(), - self::CACHE_FUNCTION => array() - ); - - $this->options = $options; - - $this->backend = $backend - ->setBroker($this) - ->setStoringTokenStreams((bool) ($options & self::OPTION_SAVE_TOKEN_STREAM)); - } - - /** - * Returns broker/parser options. - * - * @return integer - */ - public function getOptions() - { - return $this->options; - } - - /** - * Returns if a particular option setting is set. - * - * @param integer $option Option setting - * @return boolean - */ - public function isOptionSet($option) - { - return (bool) ($this->options & $option); - } - - /** - * Parses a string with the PHP source code using the given file name and returns the appropriate reflection object. - * - * @param string $source PHP source code - * @param string $fileName Used file name - * @param boolean $returnReflectionFile Returns the appropriate \TokenReflection\ReflectionFile instance(s) - * @return boolean|\TokenReflection\ReflectionFile - */ - public function processString($source, $fileName, $returnReflectionFile = false) - { - if ($this->backend->isFileProcessed($fileName)) { - $tokens = $this->backend->getFileTokens($fileName); - } else { - $tokens = new Stream\StringStream($source, $fileName); - } - - $reflectionFile = new ReflectionFile($tokens, $this); - if (!$this->backend->isFileProcessed($fileName)) { - $this->backend->addFile($tokens, $reflectionFile); - - // Clear the cache - leave only tokenized reflections - foreach ($this->cache as $type => $cached) { - if (!empty($cached)) { - $this->cache[$type] = array_filter($cached, function(IReflection $reflection) { - return $reflection->isTokenized(); - }); - } - } - } - - return $returnReflectionFile ? $reflectionFile : true; - } - - /** - * Parses a file and returns the appropriate reflection object. - * - * @param string $fileName Filename - * @param boolean $returnReflectionFile Returns the appropriate \TokenReflection\ReflectionFile instance(s) - * @return boolean|\TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\BrokerException If the file could not be processed. - */ - public function processFile($fileName, $returnReflectionFile = false) - { - try { - if ($this->backend->isFileProcessed($fileName)) { - $tokens = $this->backend->getFileTokens($fileName); - } else { - $tokens = new Stream\FileStream($fileName); - } - - $reflectionFile = new ReflectionFile($tokens, $this); - if (!$this->backend->isFileProcessed($fileName)) { - $this->backend->addFile($tokens, $reflectionFile); - - // Clear the cache - leave only tokenized reflections - foreach ($this->cache as $type => $cached) { - if (!empty($cached)) { - $this->cache[$type] = array_filter($cached, function(IReflection $reflection) { - return $reflection->isTokenized(); - }); - } - } - } - - return $returnReflectionFile ? $reflectionFile : true; - } catch (Exception\ParseException $e) { - throw $e; - } catch (Exception\StreamException $e) { - throw new Exception\BrokerException($this, 'Could not process the file.', 0, $e); - } - } - - /** - * Processes a PHAR archive. - * - * @param string $fileName Archive filename. - * @param boolean $returnReflectionFile Returns the appropriate \TokenReflection\ReflectionFile instance(s) - * @return boolean|array of \TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\BrokerException If the PHAR PHP extension is not loaded. - * @throws \TokenReflection\Exception\BrokerException If the given archive could not be read. - * @throws \TokenReflection\Exception\BrokerException If the given archive could not be processed. - */ - public function processPhar($fileName, $returnReflectionFile = false) - { - if (!is_file($fileName)) { - throw new Exception\BrokerException($this, 'File does not exist.', Exception\BrokerException::DOES_NOT_EXIST); - } - - if (!extension_loaded('Phar')) { - throw new Exception\BrokerException($this, 'The PHAR PHP extension is not loaded.', Exception\BrokerException::PHP_EXT_MISSING); - } - - try { - $result = array(); - foreach (new RecursiveIteratorIterator(new \Phar($fileName)) as $entry) { - if ($entry->isFile()) { - $result[$entry->getPathName()] = $this->processFile($entry->getPathName(), $returnReflectionFile); - } - } - - return $returnReflectionFile ? $result : true; - } catch (Exception\ParseException $e) { - throw $e; - } catch (Exception\StreamException $e) { - throw new Exception\BrokerException($this, 'Could not process the archive.', 0, $e); - } - } - - /** - * Processes recursively a directory and returns an array of file reflection objects. - * - * @param string $path Directora path - * @param string|array $filters Filename filters - * @param boolean $returnReflectionFile Returns the appropriate \TokenReflection\ReflectionFile instance(s) - * @return boolean|array of \TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\BrokerException If the given directory does not exist. - * @throws \TokenReflection\Exception\BrokerException If the given directory could not be processed. - */ - public function processDirectory($path, $filters = array(), $returnReflectionFile = false) - { - $realPath = realpath($path); - if (!is_dir($realPath)) { - throw new Exception\BrokerException($this, 'File does not exist.', Exception\BrokerException::DOES_NOT_EXIST); - } - - try { - $result = array(); - foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realPath)) as $entry) { - if ($entry->isFile()) { - $process = empty($filters); - if (!$process) { - foreach ((array) $filters as $filter) { - $whitelisting = '!' !== $filter{0}; - if (fnmatch($whitelisting ? $filter : substr($filter, 1), $entry->getPathName(), FNM_NOESCAPE)) { - $process = $whitelisting; - } - } - } - - if ($process) { - $result[$entry->getPathName()] = $this->processFile($entry->getPathName(), $returnReflectionFile); - } - } - } - - return $returnReflectionFile ? $result : true; - } catch (Exception\ParseException $e) { - throw $e; - } catch (Exception\StreamException $e) { - throw new Exception\BrokerException($this, 'Could not process the directory.', 0, $e); - } - } - - /** - * Process a file, directory or a PHAR archive. - * - * @param string $path Path - * @param boolean $returnReflectionFile Returns the appropriate \TokenReflection\ReflectionFile instance(s) - * @return boolean|array|\TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\BrokerException If the target does not exist. - */ - public function process($path, $returnReflectionFile = false) - { - if (is_dir($path)) { - return $this->processDirectory($path, array(), $returnReflectionFile); - } elseif (is_file($path)) { - if (preg_match('~\\.phar(?:$|\\.)~i', $path)) { - return $this->processPhar($path, $returnReflectionFile); - } - - return $this->processFile($path, $returnReflectionFile); - } else { - throw new Exception\BrokerException($this, 'The given directory/file does not exist.', Exception\BrokerException::DOES_NOT_EXIST); - } - } - - /** - * Returns if the broker contains a namespace of the given name. - * - * @param string $namespaceName Namespace name - * @return boolean - */ - public function hasNamespace($namespaceName) - { - return isset($this->cache[self::CACHE_NAMESPACE][$namespaceName]) || $this->backend->hasNamespace($namespaceName); - } - - /** - * Returns a reflection object of the given namespace. - * - * @param string $namespaceName Namespace name - * @return \TokenReflection\ReflectionNamespace|null - */ - public function getNamespace($namespaceName) - { - $namespaceName = ltrim($namespaceName, '\\'); - - if (isset($this->cache[self::CACHE_NAMESPACE][$namespaceName])) { - return $this->cache[self::CACHE_NAMESPACE][$namespaceName]; - } - - $namespace = $this->backend->getNamespace($namespaceName); - if (null !== $namespace) { - $this->cache[self::CACHE_NAMESPACE][$namespaceName] = $namespace; - } - - return $namespace; - } - - /** - * Returns if the broker contains a class of the given name. - * - * @param string $className Class name - * @return boolean - */ - public function hasClass($className) - { - return isset($this->cache[self::CACHE_CLASS][$className]) || $this->backend->hasClass($className); - } - - /** - * Returns a reflection object of the given class (FQN expected). - * - * @param string $className CLass bame - * @return \TokenReflection\ReflectionClass|null - */ - public function getClass($className) - { - $className = ltrim($className, '\\'); - - if (isset($this->cache[self::CACHE_CLASS][$className])) { - return $this->cache[self::CACHE_CLASS][$className]; - } - - $this->cache[self::CACHE_CLASS][$className] = $this->backend->getClass($className); - return $this->cache[self::CACHE_CLASS][$className]; - } - - /** - * Returns all classes from all namespaces. - * - * @param integer $types Returned class types (multiple values may be OR-ed) - * @return array - */ - public function getClasses($types = Broker\Backend::TOKENIZED_CLASSES) - { - return $this->backend->getClasses($types); - } - - /** - * Returns if the broker contains a constant of the given name. - * - * @param string $constantName Constant name - * @return boolean - */ - public function hasConstant($constantName) - { - return isset($this->cache[self::CACHE_CONSTANT][$constantName]) || $this->backend->hasConstant($constantName); - } - - /** - * Returns a reflection object of a constant (FQN expected). - * - * @param string $constantName Constant name - * @return \TokenReflection\ReflectionConstant|null - */ - public function getConstant($constantName) - { - $constantName = ltrim($constantName, '\\'); - - if (isset($this->cache[self::CACHE_CONSTANT][$constantName])) { - return $this->cache[self::CACHE_CONSTANT][$constantName]; - } - - if ($constant = $this->backend->getConstant($constantName)) { - $this->cache[self::CACHE_CONSTANT][$constantName] = $constant; - } - - return $constant; - } - - /** - * Returns all constants from all namespaces. - * - * @return array - */ - public function getConstants() - { - return $this->backend->getConstants(); - } - - /** - * Returns if the broker contains a function of the given name. - * - * @param string $functionName Function name - * @return boolean - */ - public function hasFunction($functionName) - { - return isset($this->cache[self::CACHE_FUNCTION][$functionName]) || $this->backend->hasFunction($functionName); - } - - /** - * Returns a reflection object of a function (FQN expected). - * - * @param string $functionName Function name - * @return \TokenReflection\ReflectionFunction|null - */ - public function getFunction($functionName) - { - $functionName = ltrim($functionName, '\\'); - - if (isset($this->cache[self::CACHE_FUNCTION][$functionName])) { - return $this->cache[self::CACHE_FUNCTION][$functionName]; - } - - if ($function = $this->backend->getFunction($functionName)) { - $this->cache[self::CACHE_FUNCTION][$functionName] = $function; - } - - return $function; - } - - /** - * Returns all functions from all namespaces. - * - * @return array - */ - public function getFunctions() - { - return $this->backend->getFunctions(); - } - - /** - * Returns if the broker contains a file reflection of the given name. - * - * @param string $fileName File name - * @return boolean - */ - public function hasFile($fileName) - { - return $this->backend->hasFile($fileName); - } - - /** - * Returns a reflection object of a file. - * - * @param string $fileName File name - * @return \TokenReflection\ReflectionFile|null - */ - public function getFile($fileName) - { - return $this->backend->getFile($fileName); - } - - /** - * Returns all processed files reflections. - * - * @return array - */ - public function getFiles() - { - return $this->backend->getFiles(); - } - - /** - * Returns an array of tokens from a processed file. - * - * @param string $fileName File name - * @return \TokenReflection\Stream\StreamBase|null - */ - public function getFileTokens($fileName) - { - return $this->backend->getFileTokens($fileName); - } - - /** - * Returns a real system path. - * - * @param string $path Source path - * @return string|boolean - */ - public static function getRealPath($path) - { - if (0 === strpos($path, 'phar://')) { - return is_file($path) || is_dir($path) ? $path : false; - } else { - return realpath($path); - } - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Broker/Backend.php b/apigen/libs/TokenReflection/TokenReflection/Broker/Backend.php deleted file mode 100644 index 2c36a306d69..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Broker/Backend.php +++ /dev/null @@ -1,212 +0,0 @@ -files[$fileName]); - } - - /** - * Returns a file reflection. - * - * @param string $fileName File name - * @return \TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\BrokerException If the requested file has not been processed - */ - public function getFile($fileName) - { - if (!isset($this->files[$fileName])) { - throw new Exception\BrokerException($this->getBroker(), sprintf('File "%s" has not been processed.', $fileName), Exception\BrokerException::DOES_NOT_EXIST); - } - - return $this->files[$fileName]; - } - - /** - * Returns file reflections. - * - * @return array - */ - public function getFiles() - { - return $this->files; - } - - /** - * Returns if there was such namespace processed (FQN expected). - * - * @param string $namespaceName Namespace name - * @return boolean - */ - public function hasNamespace($namespaceName) - { - return isset($this->namespaces[ltrim($namespaceName, '\\')]); - } - - /** - * Returns a reflection object of the given namespace. - * - * @param string $namespaceName Namespace name - * @return \TokenReflection\IReflectionNamespace - * @throws \TokenReflection\Exception\BrokerException If the requested namespace does not exist. - */ - public function getNamespace($namespaceName) - { - if (!isset($this->namespaces[TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME])) { - $this->namespaces[TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME] = new TokenReflection\ReflectionNamespace(TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME, $this->broker); - } - - $namespaceName = ltrim($namespaceName, '\\'); - if (!isset($this->namespaces[$namespaceName])) { - throw new Exception\BrokerException($this->getBroker(), sprintf('Namespace %s does not exist.', $namespaceName), Exception\BrokerException::DOES_NOT_EXIST); - } - - return $this->namespaces[$namespaceName]; - } - - /** - * Returns all present namespaces. - * - * @return array - */ - public function getNamespaces() - { - return $this->namespaces; - } - - /** - * Returns if there was such class processed (FQN expected). - * - * @param string $className Class name - * @return boolean - */ - public function hasClass($className) - { - $className = ltrim($className, '\\'); - if ($pos = strrpos($className, '\\')) { - $namespace = substr($className, 0, $pos); - - if (!isset($this->namespaces[$namespace])) { - return false; - } - - $namespace = $this->getNamespace($namespace); - $className = substr($className, $pos + 1); - } else { - $namespace = $this->getNamespace(TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME); - } - - return $namespace->hasClass($className); - } - - /** - * Returns a reflection object of the given class (FQN expected). - * - * @param string $className CLass bame - * @return \TokenReflection\IReflectionClass - */ - public function getClass($className) - { - if (empty($this->declaredClasses)) { - $this->declaredClasses = array_flip(array_merge(get_declared_classes(), get_declared_interfaces())); - } - - $className = ltrim($className, '\\'); - try { - $ns = $this->getNamespace( - ($boundary = strrpos($className, '\\')) - // Class within a namespace - ? substr($className, 0, $boundary) - // Class without a namespace - : TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME - ); - - return $ns->getClass($className); - } catch (Exception\BaseException $e) { - if (isset($this->declaredClasses[$className])) { - $reflection = new Php\ReflectionClass($className, $this->broker); - if ($reflection->isInternal()) { - return $reflection; - } - } - - return new Dummy\ReflectionClass($className, $this->broker); - } - } - - /** - * Returns all classes from all namespaces. - * - * @param integer $type Returned class types (multiple values may be OR-ed) - * @return array - */ - public function getClasses($type = self::TOKENIZED_CLASSES) - { - if (null === $this->allClasses) { - $this->allClasses = $this->parseClassLists(); - } - - $result = array(); - foreach ($this->allClasses as $classType => $classes) { - if ($type & $classType) { - $result = array_merge($result, $classes); - } - } - return $result; - } - - /** - * Returns if there was such constant processed (FQN expected). - * - * @param string $constantName Constant name - * @return boolean - */ - public function hasConstant($constantName) - { - $constantName = ltrim($constantName, '\\'); - - if ($pos = strpos($constantName, '::')) { - $className = substr($constantName, 0, $pos); - $constantName = substr($constantName, $pos + 2); - - if (!$this->hasClass($className)) { - return false; - } - - $parent = $this->getClass($className); - } else { - if ($pos = strrpos($constantName, '\\')) { - $namespace = substr($constantName, 0, $pos); - if (!$this->hasNamespace($namespace)) { - return false; - } - - $parent = $this->getNamespace($namespace); - $constantName = substr($constantName, $pos + 1); - } else { - $parent = $this->getNamespace(TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME); - } - } - - return $parent->hasConstant($constantName); - } - - /** - * Returns a reflection object of a constant (FQN expected). - * - * @param string $constantName Constant name - * @return \TokenReflection\IReflectionConstant - * @throws \TokenReflection\Exception\RuntimeException If the requested constant does not exist. - */ - public function getConstant($constantName) - { - static $declared = array(); - if (empty($declared)) { - $declared = get_defined_constants(); - } - - if ($boundary = strpos($constantName, '::')) { - // Class constant - $className = substr($constantName, 0, $boundary); - $constantName = substr($constantName, $boundary + 2); - - return $this->getClass($className)->getConstantReflection($constantName); - } - - try { - $constantName = ltrim($constantName, '\\'); - if ($boundary = strrpos($constantName, '\\')) { - $ns = $this->getNamespace(substr($constantName, 0, $boundary)); - $constantName = substr($constantName, $boundary + 1); - } else { - $ns = $this->getNamespace(TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME); - } - - return $ns->getConstant($constantName); - } catch (Exception\BaseException $e) { - if (isset($declared[$constantName])) { - $reflection = new Php\ReflectionConstant($constantName, $declared[$constantName], $this->broker); - if ($reflection->isInternal()) { - return $reflection; - } - } - - throw new Exception\BrokerException($this->getBroker(), sprintf('Constant %s does not exist.', $constantName), Exception\BrokerException::DOES_NOT_EXIST); - } - } - - /** - * Returns all constants from all namespaces. - * - * @return array - */ - public function getConstants() - { - if (null === $this->allConstants) { - $this->allConstants = array(); - foreach ($this->namespaces as $namespace) { - foreach ($namespace->getConstants() as $constant) { - $this->allConstants[$constant->getName()] = $constant; - } - } - } - - return $this->allConstants; - } - - /** - * Returns if there was such function processed (FQN expected). - * - * @param string $functionName Function name - * @return boolean - */ - public function hasFunction($functionName) - { - $functionName = ltrim($functionName, '\\'); - if ($pos = strrpos($functionName, '\\')) { - $namespace = substr($functionName, 0, $pos); - if (!isset($this->namespaces[$namespace])) { - return false; - } - - $namespace = $this->getNamespace($namespace); - $functionName = substr($functionName, $pos + 1); - } else { - $namespace = $this->getNamespace(TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME); - } - - return $namespace->hasFunction($functionName); - } - - /** - * Returns a reflection object of a function (FQN expected). - * - * @param string $functionName Function name - * @return \TokenReflection\IReflectionFunction - * @throws \TokenReflection\Exception\RuntimeException If the requested function does not exist. - */ - public function getFunction($functionName) - { - static $declared = array(); - if (empty($declared)) { - $functions = get_defined_functions(); - $declared = array_flip($functions['internal']); - } - - $functionName = ltrim($functionName, '\\'); - try { - $ns = $this->getNamespace( - ($boundary = strrpos($functionName, '\\')) - // Function within a namespace - ? substr($functionName, 0, $boundary) - // Function wihout a namespace - : TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME - ); - - return $ns->getFunction($functionName); - } catch (Exception\BaseException $e) { - if (isset($declared[$functionName])) { - return new Php\ReflectionFunction($functionName, $this->broker); - } - - throw new Exception\BrokerException($this->getBroker(), sprintf('Function %s does not exist.', $functionName), Exception\BrokerException::DOES_NOT_EXIST); - } - } - - /** - * Returns all functions from all namespaces. - * - * @return array - */ - public function getFunctions() - { - if (null === $this->allFunctions) { - $this->allFunctions = array(); - foreach ($this->namespaces as $namespace) { - foreach ($namespace->getFunctions() as $function) { - $this->allFunctions[$function->getName()] = $function; - } - } - } - - return $this->allFunctions; - } - - /** - * Returns if the given file was already processed. - * - * @param string $fileName File name - * @return boolean - */ - public function isFileProcessed($fileName) - { - return isset($this->tokenStreams[Broker::getRealPath($fileName)]); - } - - /** - * Returns an array of tokens for a particular file. - * - * @param string $fileName File name - * @return \TokenReflection\Stream\StreamBase - * @throws \TokenReflection\Exception\BrokerException If the requested file was not processed. - */ - public function getFileTokens($fileName) - { - $realName = Broker::getRealPath($fileName); - if (!isset($this->tokenStreams[$realName])) { - throw new Exception\BrokerException($this->getBroker(), sprintf('File "%s" was not processed yet.', $fileName), Exception\BrokerException::DOES_NOT_EXIST); - } - - return true === $this->tokenStreams[$realName] ? new FileStream($realName) : $this->tokenStreams[$realName]; - } - - /** - * Adds a file to the backend storage. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token stream - * @param \TokenReflection\ReflectionFile $file File reflection object - * @return \TokenReflection\Broker\Backend\Memory - */ - public function addFile(TokenReflection\Stream\StreamBase $tokenStream, TokenReflection\ReflectionFile $file) - { - $this->tokenStreams[$file->getName()] = $this->storingTokenStreams ? $tokenStream : true; - $this->files[$file->getName()] = $file; - - $errors = array(); - - foreach ($file->getNamespaces() as $fileNamespace) { - try { - $namespaceName = $fileNamespace->getName(); - if (!isset($this->namespaces[$namespaceName])) { - $this->namespaces[$namespaceName] = new TokenReflection\ReflectionNamespace($namespaceName, $file->getBroker()); - } - - $this->namespaces[$namespaceName]->addFileNamespace($fileNamespace); - } catch (Exception\FileProcessingException $e) { - $errors = array_merge($errors, $e->getReasons()); - } catch (\Exception $e) { - echo $e->getTraceAsString(); - die($e->getMessage()); - } - } - - // Reset all-*-cache - $this->allClasses = null; - $this->allFunctions = null; - $this->allConstants = null; - - if (!empty($errors)) { - throw new Exception\FileProcessingException($errors, $file); - } - - return $this; - } - - /** - * Sets the reflection broker instance. - * - * @param \TokenReflection\Broker $broker Reflection broker - * @return \TokenReflection\Broker\Backend\Memory - */ - public function setBroker(Broker $broker) - { - $this->broker = $broker; - return $this; - } - - /** - * Returns the reflection broker instance. - * - * @return \TokenReflection\Broker $broker Reflection broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Sets if token streams are stored in the backend. - * - * @param boolean $store - * @return \TokenReflection\Broker\Backend - */ - public function setStoringTokenStreams($store) - { - $this->storingTokenStreams = (bool) $store; - return $this; - } - - /** - * Returns if token streams are stored in the backend. - * - * @return boolean - */ - public function getStoringTokenStreams() - { - return $this->storingTokenStreams; - } - - /** - * Prepares and returns used class lists. - * - * @return array - */ - protected function parseClassLists() - { - // Initialize the all-classes-cache - $allClasses = array( - self::TOKENIZED_CLASSES => array(), - self::INTERNAL_CLASSES => array(), - self::NONEXISTENT_CLASSES => array() - ); - - foreach ($this->namespaces as $namespace) { - foreach ($namespace->getClasses() as $class) { - $allClasses[self::TOKENIZED_CLASSES][$class->getName()] = $class; - } - } - - foreach ($allClasses[self::TOKENIZED_CLASSES] as $className => $class) { - foreach (array_merge($class->getParentClasses(), $class->getInterfaces()) as $parent) { - if ($parent->isInternal()) { - $allClasses[self::INTERNAL_CLASSES][$parent->getName()] = $parent; - } elseif (!$parent->isTokenized()) { - $allClasses[self::NONEXISTENT_CLASSES][$parent->getName()] = $parent; - } - } - } - - return $allClasses; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Dummy/ReflectionClass.php b/apigen/libs/TokenReflection/TokenReflection/Dummy/ReflectionClass.php deleted file mode 100644 index d8322a7e85b..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Dummy/ReflectionClass.php +++ /dev/null @@ -1,1091 +0,0 @@ -name = ltrim($className, '\\'); - $this->broker = $broker; - } - - /** - * Returns the name (FQN). - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->name; - } - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - $pos = strrpos($this->name, '\\'); - return false === $pos ? $this->name : substr($this->name, $pos + 1); - } - - /** - * Returns the namespace name. - * - * @return string - */ - public function getNamespaceName() - { - $pos = strrpos($this->name, '\\'); - return false === $pos ? '' : substr($this->name, 0, $pos); - } - - /** - * Returns if the class is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return false !== strrpos($this->name, '\\'); - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return array(); - } - - /** - * Returns the PHP extension reflection. - * - * @return null - */ - public function getExtension() - { - return null; - } - - /** - * Returns the PHP extension name. - * - * @return boolean - */ - public function getExtensionName() - { - return false; - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return null - */ - public function getFileName() - { - return null; - } - - /** - * Returns a file reflection. - * - * @return \TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\RuntimeException If the file is not stored inside the broker - */ - public function getFileReflection() - { - throw new Exception\BrokerException($this->getBroker(), sprintf('Class was not parsed from a file', $this->getName()), Exception\BrokerException::UNSUPPORTED); - } - - /** - * Returns the definition start line number in the file. - * - * @return null - */ - public function getStartLine() - { - return null; - } - - /** - * Returns the definition end line number in the file. - * - * @return null - */ - public function getEndLine() - { - return null; - } - - /** - * Returns the appropriate docblock definition. - * - * @return boolean - */ - public function getDocComment() - { - return false; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns modifiers. - * - * @return integer - */ - public function getModifiers() - { - return 0; - } - - /** - * Returns if the class is abstract. - * - * @return boolean - */ - public function isAbstract() - { - return false; - } - - /** - * Returns if the class is final. - * - * @return boolean - */ - public function isFinal() - { - return false; - } - - /** - * Returns if the class is an interface. - * - * @return boolean - */ - public function isInterface() - { - return false; - } - - /** - * Returns if the class is an exception or its descendant. - * - * @return boolean - */ - public function isException() - { - return false; - } - - /** - * Returns if it is possible to create an instance of this class. - * - * @return boolean - */ - public function isInstantiable() - { - return false; - } - - /** - * Returns traits used by this class. - * - * @return array - */ - public function getTraits() - { - return array(); - } - - /** - * Returns traits used by this class and not its parents. - * - * @return array - */ - public function getOwnTraits() - { - return array(); - } - - /** - * Returns names of used traits. - * - * @return array - */ - public function getTraitNames() - { - return array(); - } - - /** - * Returns traits used by this class and not its parents. - * - * @return array - */ - public function getOwnTraitNames() - { - return array(); - } - - /** - * Returns method aliases from traits. - * - * @return array - */ - public function getTraitAliases() - { - return array(); - } - - /** - * Returns if the class is a trait. - * - * @return boolean - */ - public function isTrait() - { - return false; - } - - /** - * Returns if the class uses a particular trait. - * - * @param \ReflectionClass|\TokenReflection\IReflectionClass|string $trait Trait reflection or name - * @return boolean - */ - public function usesTrait($trait) - { - return false; - } - - /** - * Returns if objects of this class are cloneable. - * - * Introduced in PHP 5.4. - * - * @return boolean - * @see http://svn.php.net/viewvc/php/php-src/trunk/ext/reflection/php_reflection.c?revision=307971&view=markup#l4059 - */ - public function isCloneable() - { - return false; - } - - /** - * Returns if the class is iterateable. - * - * Returns true if the class implements the Traversable interface. - * - * @return boolean - */ - public function isIterateable() - { - return false; - } - - /** - * Returns if the reflection object is internal. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns if the reflection object is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return false; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return false; - } - - /** - * Returns if the current class is a subclass of the given class. - * - * @param string|object $class Class name or reflection object - * @return boolean - */ - public function isSubclassOf($class) - { - return false; - } - - /** - * Returns the parent class reflection. - * - * @return null - */ - public function getParentClass() - { - return false; - } - - /** - * Returns the parent classes reflections. - * - * @return array - */ - public function getParentClasses() - { - return array(); - } - - /** - * Returns the parent classes names. - * - * @return array - */ - public function getParentClassNameList() - { - return array(); - } - - /** - * Returns the parent class reflection. - * - * @return null - */ - public function getParentClassName() - { - return null; - } - - /** - * Returns if the class implements the given interface. - * - * @param string|object $interface Interface name or reflection object - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If the provided parameter is not an interface. - */ - public function implementsInterface($interface) - { - if (is_object($interface)) { - if (!$interface instanceof IReflectionClass) { - throw new Exception\RuntimeException(sprintf('Parameter must be a string or an instance of class reflection, "%s" provided.', get_class($interface)), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $interfaceName = $interface->getName(); - - if (!$interface->isInterface()) { - throw new Exception\RuntimeException(sprintf('"%s" is not an interface.', $interfaceName), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - } - - // Only validation, always returns false - return false; - } - - /** - * Returns interface reflections. - * - * @return array - */ - public function getInterfaces() - { - return array(); - } - - /** - * Returns interface names. - * - * @return array - */ - public function getInterfaceNames() - { - return array(); - } - - /** - * Returns interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaces() - { - return array(); - } - - /** - * Returns names of interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaceNames() - { - return array(); - } - - /** - * Returns the class constructor reflection. - * - * @return null - */ - public function getConstructor() - { - return null; - } - - /** - * Returns the class desctructor reflection. - * - * @return null - */ - public function getDestructor() - { - return null; - } - - /** - * Returns if the class implements the given method. - * - * @param string $name Method name - * @return boolean - */ - public function hasMethod($name) - { - return false; - } - - /** - * Returns a method reflection. - * - * @param string $name Method name - * @throws \TokenReflection\Exception\RuntimeException If the requested method does not exist. - */ - public function getMethod($name) - { - throw new Exception\RuntimeException(sprintf('There is no method "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns method reflections. - * - * @param integer $filter Methods filter - * @return array - */ - public function getMethods($filter = null) - { - return array(); - } - - /** - * Returns if the class implements (and not its parents) the given method. - * - * @param string $name Method name - * @return boolean - */ - public function hasOwnMethod($name) - { - return false; - } - - /** - * Returns methods declared by this class, not its parents. - * - * @param integer $filter Methods filter - * @return array - */ - public function getOwnMethods($filter = null) - { - return array(); - } - - /** - * Returns if the class imports the given method from traits. - * - * @param string $name Method name - * @return boolean - */ - public function hasTraitMethod($name) - { - return false; - } - - /** - * Returns method reflections imported from traits. - * - * @param integer $filter Methods filter - * @return array - */ - public function getTraitMethods($filter = null) - { - return array(); - } - - /** - * Returns if the class defines the given constant. - * - * @param string $name Constant name. - * @return boolean - */ - public function hasConstant($name) - { - return false; - } - - /** - * Returns a constant value. - * - * @param string $name Constant name - * @throws \TokenReflection\Exception\RuntimeException If the requested constant does not exist. - */ - public function getConstant($name) - { - throw new Exception\RuntimeException(sprintf('There is no constant "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @throws \TokenReflection\Exception\RuntimeException If the requested constant does not exist. - */ - public function getConstantReflection($name) - { - throw new Exception\RuntimeException(sprintf('There is no constant "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns an array of constant values. - * - * @return array - */ - public function getConstants() - { - return array(); - } - - /** - * Returns an array of constant reflections. - * - * @return array - */ - public function getConstantReflections() - { - return array(); - } - - /** - * Returns if the class (and not its parents) defines the given constant. - * - * @param string $name Constant name. - * @return boolean - */ - public function hasOwnConstant($name) - { - return false; - } - - /** - * Returns constants declared by this class, not its parents. - * - * @return array - */ - public function getOwnConstants() - { - return array(); - } - - /** - * Returns an array of constant reflections defined by this class not its parents. - * - * @return array - */ - public function getOwnConstantReflections() - { - return array(); - } - - /** - * Returns default properties. - * - * @return array - */ - public function getDefaultProperties() - { - return array(); - } - - /** - * Returns if the class implements the given property. - * - * @param string $name Property name - * @return boolean - */ - public function hasProperty($name) - { - return false; - } - - /** - * Returns class properties. - * - * @param integer $filter Property types - * @return array - */ - public function getProperties($filter = null) - { - return array(); - } - - /** - * Return a property reflections. - * - * @param string $name Property name - * @throws \TokenReflection\Exception\RuntimeException If the requested property does not exist. - */ - public function getProperty($name) - { - throw new Exception\RuntimeException(sprintf('There is no property "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns if the class (and not its parents) implements the given property. - * - * @param string $name Property name - * @return boolean - */ - public function hasOwnProperty($name) - { - return false; - } - - /** - * Returns properties declared by this class, not its parents. - * - * @param integer $filter Properties filter - * @return array - */ - public function getOwnProperties($filter = null) - { - return array(); - } - - /** - * Returns if the class imports the given property from traits. - * - * @param string $name Property name - * @return boolean - */ - public function hasTraitProperty($name) - { - return false; - } - - /** - * Returns property reflections imported from traits. - * - * @param integer $filter Properties filter - * @return array - */ - public function getTraitProperties($filter = null) - { - return array(); - } - - /** - * Returns static properties reflections. - * - * @return array - */ - public function getStaticProperties() - { - return array(); - } - - /** - * Returns a value of a static property. - * - * @param string $name Property name - * @param mixed $default Default value - * @throws \TokenReflection\Exception\RuntimeException If the requested static property does not exist. - */ - public function getStaticPropertyValue($name, $default = null) - { - throw new Exception\RuntimeException(sprintf('There is no static property "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns reflections of direct subclasses. - * - * @return array - */ - public function getDirectSubclasses() - { - return array(); - } - - /** - * Returns names of direct subclasses. - * - * @return array - */ - public function getDirectSubclassNames() - { - return array(); - } - - /** - * Returns reflections of indirect subclasses. - * - * @return array - */ - public function getIndirectSubclasses() - { - return array(); - } - - /** - * Returns names of indirect subclasses. - * - * @return array - */ - public function getIndirectSubclassNames() - { - return array(); - } - - /** - * Returns reflections of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementers() - { - return array(); - } - - /** - * Returns names of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementerNames() - { - return array(); - } - - /** - * Returns reflections of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementers() - { - return array(); - } - - /** - * Returns names of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementerNames() - { - return array(); - } - - /** - * Returns if the given object is an instance of this class. - * - * @param object $object Instance - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If the provided argument is not an object. - */ - public function isInstance($object) - { - if (!is_object($object)) { - throw new Exception\RuntimeException(sprintf('Parameter must be a class instance, "%s" provided.', gettype($object)), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - return $this->name === get_class($object) || is_subclass_of($object, $this->name); - } - - /** - * Creates a new class instance without using a constructor. - * - * @return object - * @throws \TokenReflection\Exception\RuntimeException If the class inherits from an internal class. - */ - public function newInstanceWithoutConstructor() - { - if (!class_exists($this->name, true)) { - throw new Exception\RuntimeException('Could not create an instance; class does not exist.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $reflection = new \TokenReflection\Php\ReflectionClass($this->name, $this->getBroker()); - return $reflection->newInstanceWithoutConstructor(); - } - - /** - * Creates a new instance using variable number of parameters. - * - * Use any number of constructor parameters as function parameters. - * - * @param mixed $args - * @return object - */ - public function newInstance($args) - { - return $this->newInstanceArgs(func_get_args()); - } - - /** - * Creates a new instance using an array of parameters. - * - * @param array $args Array of constructor parameters - * @return object - * @throws \TokenReflection\Exception\RuntimeException If the required class does not exist. - */ - public function newInstanceArgs(array $args = array()) - { - if (!class_exists($this->name, true)) { - throw new Exception\RuntimeException('Could not create an instance of class; class does not exist.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $reflection = new InternalReflectionClass($this->name); - return $reflection->newInstanceArgs($args); - } - - /** - * Sets a static property value. - * - * @param string $name Property name - * @param mixed $value Property value - * @throws \TokenReflection\Exception\RuntimeException If the requested static property does not exist. - */ - public function setStaticPropertyValue($name, $value) - { - throw new Exception\RuntimeException(sprintf('There is no static property "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - return sprintf( - "Class|Interface [ class|interface %s ] {\n %s%s%s%s%s\n}\n", - $this->getName(), - "\n\n - Constants [0] {\n }", - "\n\n - Static properties [0] {\n }", - "\n\n - Static methods [0] {\n }", - "\n\n - Properties [0] {\n }", - "\n\n - Methods [0] {\n }" - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string|object $className Class name or class instance - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $className, $return = false) - { - TokenReflection\ReflectionClass::export($broker, $className, $return); - } - - /** - * Outputs the reflection subject source code. - * - * @return string - */ - public function getSource() - { - return ''; - } - - /** - * Returns the start position in the file token stream. - * - * @return integer - */ - public function getStartPosition() - { - return -1; - } - - /** - * Returns the end position in the file token stream. - * - * @return integer - */ - public function getEndPosition() - { - return -1; - } - - /** - * Returns if the class definition is complete. - * - * Dummy classes never have the definition complete. - * - * @return boolean - */ - public function isComplete() - { - return false; - } - - /** - * Returns if the class definition is valid. - * - * Dummy classes are always valid. - * - * @return boolean - */ - public function isValid() - { - return true; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return ReflectionBase::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return ReflectionBase::exists($this, $key); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Exception/BaseException.php b/apigen/libs/TokenReflection/TokenReflection/Exception/BaseException.php deleted file mode 100644 index aaebfe118f0..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Exception/BaseException.php +++ /dev/null @@ -1,102 +0,0 @@ -getDetail(); - - return sprintf( - "exception '%s'%s in %s on line %d\n%s\nStack trace:\n%s", - get_class($this), - $this->getMessage() ? " with message '" . $this->getMessage() . "'" : '', - $this->getFile(), - $this->getLine(), - empty($detail) ? '' : $detail . "\n", - $this->getTraceAsString() - ); - } - - /** - * Returns the exception details as string. - * - * @return string - */ - final public function __toString() - { - $output = ''; - - if ($ex = $this->getPrevious()) { - $output .= (string) $ex . "\n\nNext "; - } - - return $output . $this->getOutput() . "\n"; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Exception/BrokerException.php b/apigen/libs/TokenReflection/TokenReflection/Exception/BrokerException.php deleted file mode 100644 index eb7e21d0c6a..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Exception/BrokerException.php +++ /dev/null @@ -1,66 +0,0 @@ -broker = $broker; - } - - /** - * Returns the current Broker. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Returns an exception description detail. - * - * @return string - */ - public function getDetail() - { - return ''; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Exception/FileProcessingException.php b/apigen/libs/TokenReflection/TokenReflection/Exception/FileProcessingException.php deleted file mode 100644 index 48da374153d..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Exception/FileProcessingException.php +++ /dev/null @@ -1,80 +0,0 @@ -getName()), - 0, - $sender - ); - - $this->reasons = $reasons; - } - - /** - * Returns a list of reasons why the file could not be processed. - * - * @return array - */ - public function getReasons() - { - return $this->reasons; - } - - /** - * Returns an exception description detail. - * - * @return string - */ - public function getDetail() - { - if (!empty($this->reasons)) { - $reasons = array_map(function(BaseException $reason) { - if ($reason instanceof ParseException) { - return $reason->getDetail(); - } else { - return $reason->getMessage(); - } - }, $this->reasons); - - return "There were following reasons for this exception:\n" . implode("\n", $reasons); - } - - return ''; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Exception/ParseException.php b/apigen/libs/TokenReflection/TokenReflection/Exception/ParseException.php deleted file mode 100644 index c9161c6fdb3..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Exception/ParseException.php +++ /dev/null @@ -1,264 +0,0 @@ -sender = $sender; - - $token = $tokenStream->current(); - $position = $tokenStream->key(); - - if (!empty($token) && !empty($position)) { - $this->token = $token; - $this->tokenName = $tokenStream->getTokenName(); - - $line = $this->token[2]; - $min = $max = $position; - } else { - $min = $max = $tokenStream->count() - 1; - $line = $tokenStream[$min][2]; - } - - $this->exceptionLine = $line; - - static $skip = array(T_WHITESPACE => true, T_COMMENT => true, T_DOC_COMMENT => true); - - $significant = array(); - while (isset($tokenStream[$min - 1])) { - if (!isset($significant[$tokenStream[$min][2]])) { - if (self::SOURCE_LINES_AROUND <= array_sum($significant)) { - break; - } - - $significant[$tokenStream[$min][2]] = !isset($skip[$tokenStream[$min][0]]); - } else { - $significant[$tokenStream[$min][2]] |= !isset($skip[$tokenStream[$min][0]]); - } - - $min--; - } - - $significant = array(); - while (isset($tokenStream[$max + 1])) { - if (!isset($significant[$tokenStream[$max][2]])) { - if (self::SOURCE_LINES_AROUND <= array_sum($significant)) { - break; - } - - $significant[$tokenStream[$max][2]] = !isset($skip[$tokenStream[$max][0]]); - } else { - $significant[$tokenStream[$max][2]] |= !isset($skip[$tokenStream[$max][0]]); - } - - $max++; - } - - $this->scopeBoundaries = array($min, $max); - } - - /** - * Returns the token where the problem was detected or NULL if the token stream was empty or an end was reached. - * - * @return array|null - */ - public function getToken() - { - return $this->token; - } - - /** - * Returns the name of the token where the problem was detected or NULL if the token stream was empty or an end was reached. - * - * @return string|null - */ - public function getTokenName() - { - return $this->tokenName; - } - - /** - * Returns the line where the exception was thrown. - * - * @return integer - */ - public function getExceptionLine() - { - return $this->exceptionLine; - } - - /** - * Returns the file line with the token or null. - * - * @return integer|null - */ - public function getTokenLine() - { - return null === $this->token ? null : $this->token[2]; - } - - /** - * Returns the source code part around the token. - * - * @param boolean $lineNumbers Returns the source code part with line numbers - * @return string|null - */ - public function getSourcePart($lineNumbers = false) - { - if (empty($this->scopeBoundaries)) { - return null; - } - - list($lo, $hi) = $this->scopeBoundaries; - $stream = $this->getStream(); - - $code = $stream->getSourcePart($lo, $hi); - - if ($lineNumbers) { - $lines = explode("\n", $code); - - $startLine = $stream[$lo][2]; - $width = strlen($startLine + count($lines) - 1); - $errorLine = $this->token[2]; - $actualLine = $startLine; - - $code = implode( - "\n", - array_map(function($line) use (&$actualLine, $width, $errorLine) { - return ($actualLine === $errorLine ? '*' : ' ') . str_pad($actualLine++, $width, ' ', STR_PAD_LEFT) . ': ' . $line; - }, $lines) - ); - } - - return $code; - } - - /** - * Returns the reflection element that caused the exception to be raised. - * - * @return \TokenReflection\IReflection - */ - public function getSender() - { - return $this->sender; - } - - /** - * Returns an exception description detail. - * - * @return string - */ - public function getDetail() - { - if (0 === $this->getStream()->count()) { - return parent::getDetail() . 'The token stream was empty.'; - } elseif (empty($this->token)) { - return parent::getDetail() . 'The token stream was read out of its bounds.'; - } else { - return parent::getDetail() . - sprintf( - "\nThe cause of the exception was the %s token (line %s) in following part of %s source code:\n\n%s", - $this->tokenName, - $this->token[2], - $this->sender && $this->sender->getName() ? $this->sender->getPrettyName() : 'the', - $this->getSourcePart(true) - ); - } - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Exception/RuntimeException.php b/apigen/libs/TokenReflection/TokenReflection/Exception/RuntimeException.php deleted file mode 100644 index 98577794d1f..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Exception/RuntimeException.php +++ /dev/null @@ -1,72 +0,0 @@ -sender = $sender; - } - - /** - * Returns the reflection element that caused the exception to be raised. - * - * @return \TokenReflection\IReflection - */ - public function getSender() - { - return $this->sender; - } - - /** - * Returns an exception description detail. - * - * @return string - */ - public function getDetail() - { - return null === $this->sender ? '' : sprintf('Thrown when working with "%s".', $this->sender->getPrettyName()); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Exception/StreamException.php b/apigen/libs/TokenReflection/TokenReflection/Exception/StreamException.php deleted file mode 100644 index 5aa7519ea39..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Exception/StreamException.php +++ /dev/null @@ -1,96 +0,0 @@ -stream = $stream; - } - - /** - * Returns the reflection element that caused the exception to be raised. - * - * @return \TokenReflection\Stream\StreamBase - */ - public function getStream() - { - return $this->stream; - } - - /** - * Returns the processed file name. - * - * @return string - */ - public function getFileName() - { - return $this->stream->getFileName(); - } - - /** - * Returns an exception description detail. - * - * @return string - */ - public function getDetail() - { - return sprintf('Thrown when working with file "%s" token stream.', $this->getFileName()); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/IReflection.php b/apigen/libs/TokenReflection/TokenReflection/IReflection.php deleted file mode 100644 index 911dd633ab0..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/IReflection.php +++ /dev/null @@ -1,80 +0,0 @@ - 5.3.0, you can uncomment it. - * - * @return mixed - */ - // public function invoke(); - - /** - * Calls the function. - * - * @param array $args Function parameter values - * @return mixed - */ - public function invokeArgs(array $args); - - /** - * Returns the function/method as closure. - * - * @return \Closure - */ - public function getClosure(); - - /** - * Returns if the function definition is valid. - * - * That means that the source code is valid and the function name is unique within parsed files. - * - * @return boolean - */ - public function isValid(); - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases(); -} diff --git a/apigen/libs/TokenReflection/TokenReflection/IReflectionFunctionBase.php b/apigen/libs/TokenReflection/TokenReflection/IReflectionFunctionBase.php deleted file mode 100644 index 9076590cd81..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/IReflectionFunctionBase.php +++ /dev/null @@ -1,135 +0,0 @@ -name = ltrim($className, '\\'); - $this->fileName = $fileName; - $this->broker = $broker; - } - - /** - * Returns the name (FQN). - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->name; - } - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - $pos = strrpos($this->name, '\\'); - return false === $pos ? $this->name : substr($this->name, $pos + 1); - } - - /** - * Returns the namespace name. - * - * @return string - */ - public function getNamespaceName() - { - $pos = strrpos($this->name, '\\'); - return false === $pos ? '' : substr($this->name, 0, $pos); - } - - /** - * Returns if the class is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return false !== strrpos($this->name, '\\'); - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return array(); - } - - /** - * Returns the PHP extension reflection. - * - * @return null - */ - public function getExtension() - { - return null; - } - - /** - * Returns the PHP extension name. - * - * @return boolean - */ - public function getExtensionName() - { - return false; - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return null - */ - public function getFileName() - { - return $this->fileName; - } - - /** - * Returns a file reflection. - * - * @return \TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\RuntimeException If the file is not stored inside the broker - */ - public function getFileReflection() - { - throw new Exception\BrokerException($this->getBroker(), sprintf('Class was not parsed from a file', $this->getName()), Exception\BrokerException::UNSUPPORTED); - } - - /** - * Returns the definition start line number in the file. - * - * @return null - */ - public function getStartLine() - { - return null; - } - - /** - * Returns the definition end line number in the file. - * - * @return null - */ - public function getEndLine() - { - return null; - } - - /** - * Returns the appropriate docblock definition. - * - * @return boolean - */ - public function getDocComment() - { - return false; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns modifiers. - * - * @return integer - */ - public function getModifiers() - { - return 0; - } - - /** - * Returns if the class is abstract. - * - * @return boolean - */ - public function isAbstract() - { - return false; - } - - /** - * Returns if the class is final. - * - * @return boolean - */ - public function isFinal() - { - return false; - } - - /** - * Returns if the class is an interface. - * - * @return boolean - */ - public function isInterface() - { - return false; - } - - /** - * Returns if the class is an exception or its descendant. - * - * @return boolean - */ - public function isException() - { - return false; - } - - /** - * Returns if it is possible to create an instance of this class. - * - * @return boolean - */ - public function isInstantiable() - { - return false; - } - - /** - * Returns traits used by this class. - * - * @return array - */ - public function getTraits() - { - return array(); - } - - /** - * Returns traits used by this class and not its parents. - * - * @return array - */ - public function getOwnTraits() - { - return array(); - } - - /** - * Returns names of used traits. - * - * @return array - */ - public function getTraitNames() - { - return array(); - } - - /** - * Returns traits used by this class and not its parents. - * - * @return array - */ - public function getOwnTraitNames() - { - return array(); - } - - /** - * Returns method aliases from traits. - * - * @return array - */ - public function getTraitAliases() - { - return array(); - } - - /** - * Returns if the class is a trait. - * - * @return boolean - */ - public function isTrait() - { - return false; - } - - /** - * Returns if the class uses a particular trait. - * - * @param \ReflectionClass|\TokenReflection\IReflectionClass|string $trait Trait reflection or name - * @return boolean - */ - public function usesTrait($trait) - { - return false; - } - - /** - * Returns if objects of this class are cloneable. - * - * Introduced in PHP 5.4. - * - * @return boolean - * @see http://svn.php.net/viewvc/php/php-src/trunk/ext/reflection/php_reflection.c?revision=307971&view=markup#l4059 - */ - public function isCloneable() - { - return false; - } - - /** - * Returns if the class is iterateable. - * - * Returns true if the class implements the Traversable interface. - * - * @return boolean - */ - public function isIterateable() - { - return false; - } - - /** - * Returns if the reflection object is internal. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns if the reflection object is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return true; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return true; - } - - /** - * Returns if the current class is a subclass of the given class. - * - * @param string|object $class Class name or reflection object - * @return boolean - */ - public function isSubclassOf($class) - { - return false; - } - - /** - * Returns the parent class reflection. - * - * @return null - */ - public function getParentClass() - { - return false; - } - - /** - * Returns the parent classes reflections. - * - * @return array - */ - public function getParentClasses() - { - return array(); - } - - /** - * Returns the parent classes names. - * - * @return array - */ - public function getParentClassNameList() - { - return array(); - } - - /** - * Returns the parent class reflection. - * - * @return null - */ - public function getParentClassName() - { - return null; - } - - /** - * Returns if the class implements the given interface. - * - * @param string|object $interface Interface name or reflection object - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If the provided parameter is not an interface. - */ - public function implementsInterface($interface) - { - if (is_object($interface)) { - if (!$interface instanceof IReflectionClass) { - throw new Exception\RuntimeException(sprintf('Parameter must be a string or an instance of class reflection, "%s" provided.', get_class($interface)), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $interfaceName = $interface->getName(); - - if (!$interface->isInterface()) { - throw new Exception\RuntimeException(sprintf('"%s" is not an interface.', $interfaceName), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - } - - // Only validation, always returns false - return false; - } - - /** - * Returns interface reflections. - * - * @return array - */ - public function getInterfaces() - { - return array(); - } - - /** - * Returns interface names. - * - * @return array - */ - public function getInterfaceNames() - { - return array(); - } - - /** - * Returns interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaces() - { - return array(); - } - - /** - * Returns names of interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaceNames() - { - return array(); - } - - /** - * Returns the class constructor reflection. - * - * @return null - */ - public function getConstructor() - { - return null; - } - - /** - * Returns the class desctructor reflection. - * - * @return null - */ - public function getDestructor() - { - return null; - } - - /** - * Returns if the class implements the given method. - * - * @param string $name Method name - * @return boolean - */ - public function hasMethod($name) - { - return false; - } - - /** - * Returns a method reflection. - * - * @param string $name Method name - * @throws \TokenReflection\Exception\RuntimeException If the requested method does not exist. - */ - public function getMethod($name) - { - throw new Exception\RuntimeException(sprintf('There is no method "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns method reflections. - * - * @param integer $filter Methods filter - * @return array - */ - public function getMethods($filter = null) - { - return array(); - } - - /** - * Returns if the class implements (and not its parents) the given method. - * - * @param string $name Method name - * @return boolean - */ - public function hasOwnMethod($name) - { - return false; - } - - /** - * Returns methods declared by this class, not its parents. - * - * @param integer $filter Methods filter - * @return array - */ - public function getOwnMethods($filter = null) - { - return array(); - } - - /** - * Returns if the class imports the given method from traits. - * - * @param string $name Method name - * @return boolean - */ - public function hasTraitMethod($name) - { - return false; - } - - /** - * Returns method reflections imported from traits. - * - * @param integer $filter Methods filter - * @return array - */ - public function getTraitMethods($filter = null) - { - return array(); - } - - /** - * Returns if the class defines the given constant. - * - * @param string $name Constant name. - * @return boolean - */ - public function hasConstant($name) - { - return false; - } - - /** - * Returns a constant value. - * - * @param string $name Constant name - * @throws \TokenReflection\Exception\RuntimeException If the requested constant does not exist. - */ - public function getConstant($name) - { - throw new Exception\RuntimeException(sprintf('There is no constant "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @throws \TokenReflection\Exception\RuntimeException If the requested constant does not exist. - */ - public function getConstantReflection($name) - { - throw new Exception\RuntimeException(sprintf('There is no constant "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns an array of constant values. - * - * @return array - */ - public function getConstants() - { - return array(); - } - - /** - * Returns an array of constant reflections. - * - * @return array - */ - public function getConstantReflections() - { - return array(); - } - - /** - * Returns if the class (and not its parents) defines the given constant. - * - * @param string $name Constant name. - * @return boolean - */ - public function hasOwnConstant($name) - { - return false; - } - - /** - * Returns constants declared by this class, not its parents. - * - * @return array - */ - public function getOwnConstants() - { - return array(); - } - - /** - * Returns an array of constant reflections defined by this class not its parents. - * - * @return array - */ - public function getOwnConstantReflections() - { - return array(); - } - - /** - * Returns default properties. - * - * @return array - */ - public function getDefaultProperties() - { - return array(); - } - - /** - * Returns if the class implements the given property. - * - * @param string $name Property name - * @return boolean - */ - public function hasProperty($name) - { - return false; - } - - /** - * Returns class properties. - * - * @param integer $filter Property types - * @return array - */ - public function getProperties($filter = null) - { - return array(); - } - - /** - * Return a property reflections. - * - * @param string $name Property name - * @throws \TokenReflection\Exception\RuntimeException If the requested property does not exist. - */ - public function getProperty($name) - { - throw new Exception\RuntimeException(sprintf('There is no property "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns if the class (and not its parents) implements the given property. - * - * @param string $name Property name - * @return boolean - */ - public function hasOwnProperty($name) - { - return false; - } - - /** - * Returns properties declared by this class, not its parents. - * - * @param integer $filter Properties filter - * @return array - */ - public function getOwnProperties($filter = null) - { - return array(); - } - - /** - * Returns if the class imports the given property from traits. - * - * @param string $name Property name - * @return boolean - */ - public function hasTraitProperty($name) - { - return false; - } - - /** - * Returns property reflections imported from traits. - * - * @param integer $filter Properties filter - * @return array - */ - public function getTraitProperties($filter = null) - { - return array(); - } - - /** - * Returns static properties reflections. - * - * @return array - */ - public function getStaticProperties() - { - return array(); - } - - /** - * Returns a value of a static property. - * - * @param string $name Property name - * @param mixed $default Default value - * @throws \TokenReflection\Exception\RuntimeException If the requested static property does not exist. - */ - public function getStaticPropertyValue($name, $default = null) - { - throw new Exception\RuntimeException(sprintf('There is no static property "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns reflections of direct subclasses. - * - * @return array - */ - public function getDirectSubclasses() - { - return array(); - } - - /** - * Returns names of direct subclasses. - * - * @return array - */ - public function getDirectSubclassNames() - { - return array(); - } - - /** - * Returns reflections of indirect subclasses. - * - * @return array - */ - public function getIndirectSubclasses() - { - return array(); - } - - /** - * Returns names of indirect subclasses. - * - * @return array - */ - public function getIndirectSubclassNames() - { - return array(); - } - - /** - * Returns reflections of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementers() - { - return array(); - } - - /** - * Returns names of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementerNames() - { - return array(); - } - - /** - * Returns reflections of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementers() - { - return array(); - } - - /** - * Returns names of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementerNames() - { - return array(); - } - - /** - * Returns if the given object is an instance of this class. - * - * @param object $object Instance - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If the provided argument is not an object. - */ - public function isInstance($object) - { - if (!is_object($object)) { - throw new Exception\RuntimeException(sprintf('Parameter must be a class instance, "%s" provided.', gettype($object)), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - return $this->name === get_class($object) || is_subclass_of($object, $this->name); - } - - /** - * Creates a new class instance without using a constructor. - * - * @return object - * @throws \TokenReflection\Exception\RuntimeException If the class inherits from an internal class. - */ - public function newInstanceWithoutConstructor() - { - if (!class_exists($this->name, true)) { - throw new Exception\RuntimeException('Could not create an instance; class does not exist.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $reflection = new \TokenReflection\Php\ReflectionClass($this->name, $this->getBroker()); - return $reflection->newInstanceWithoutConstructor(); - } - - /** - * Creates a new instance using variable number of parameters. - * - * Use any number of constructor parameters as function parameters. - * - * @param mixed $args - * @return object - */ - public function newInstance($args) - { - return $this->newInstanceArgs(func_get_args()); - } - - /** - * Creates a new instance using an array of parameters. - * - * @param array $args Array of constructor parameters - * @return object - * @throws \TokenReflection\Exception\RuntimeException If the required class does not exist. - */ - public function newInstanceArgs(array $args = array()) - { - if (!class_exists($this->name, true)) { - throw new Exception\RuntimeException('Could not create an instance of class; class does not exist.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $reflection = new InternalReflectionClass($this->name); - return $reflection->newInstanceArgs($args); - } - - /** - * Sets a static property value. - * - * @param string $name Property name - * @param mixed $value Property value - * @throws \TokenReflection\Exception\RuntimeException If the requested static property does not exist. - */ - public function setStaticPropertyValue($name, $value) - { - throw new Exception\RuntimeException(sprintf('There is no static property "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - return sprintf( - "Class|Interface [ class|interface %s ] {\n %s%s%s%s%s\n}\n", - $this->getName(), - "\n\n - Constants [0] {\n }", - "\n\n - Static properties [0] {\n }", - "\n\n - Static methods [0] {\n }", - "\n\n - Properties [0] {\n }", - "\n\n - Methods [0] {\n }" - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string|object $className Class name or class instance - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $className, $return = false) - { - TokenReflection\ReflectionClass::export($broker, $className, $return); - } - - /** - * Outputs the reflection subject source code. - * - * @return string - */ - public function getSource() - { - return ''; - } - - /** - * Returns the start position in the file token stream. - * - * @return integer - */ - public function getStartPosition() - { - return -1; - } - - /** - * Returns the end position in the file token stream. - * - * @return integer - */ - public function getEndPosition() - { - return -1; - } - - /** - * Returns if the class definition is complete. - * - * Invalid classes are always complete. - * - * @return boolean - */ - public function isComplete() - { - return true; - } - - /** - * Returns if the class definition is valid. - * - * @return boolean - */ - public function isValid() - { - return false; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return ReflectionBase::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return ReflectionBase::exists($this, $key); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionConstant.php b/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionConstant.php deleted file mode 100644 index 6fb75bb2028..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionConstant.php +++ /dev/null @@ -1,403 +0,0 @@ -name = $name; - $this->broker = $broker; - $this->fileName = $fileName; - } - - /** - * Returns the name. - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - $pos = strrpos($this->name, '\\'); - return false === $pos ? $this->name : substr($this->name, $pos + 1); - } - - /** - * Returns the declaring class reflection. - * - * @return null - */ - public function getDeclaringClass() - { - return null; - } - - /** - * Returns the declaring class name. - * - * @return null - */ - public function getDeclaringClassName() - { - return null; - } - - /** - * Returns the namespace name. - * - * @return string - */ - public function getNamespaceName() - { - $pos = strrpos($this->name, '\\'); - return false === $pos ? '' : substr($this->name, 0, $pos); - } - - /** - * Returns if the function/method is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return false !== strpos($this->name, '\\'); - } - - /** - * Returns the PHP extension reflection. - * - * @return null - */ - public function getExtension() - { - return null; - } - - /** - * Returns the PHP extension name. - * - * @return boolean - */ - public function getExtensionName() - { - return false; - } - - /** - * Returns the appropriate source code part. - * - * @return string - */ - public function getSource() - { - return ''; - } - - /** - * Returns the start position in the file token stream. - * - * @return integer - */ - public function getStartPosition() - { - return -1; - } - - /** - * Returns the end position in the file token stream. - * - * @return integer - */ - public function getEndPosition() - { - return -1; - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return null - */ - public function getFileName() - { - return $this->fileName; - } - - /** - * Returns a file reflection. - * - * @return \TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\RuntimeException If the file is not stored inside the broker - */ - public function getFileReflection() - { - throw new Exception\BrokerException($this->getBroker(), sprintf('Constant %s was not parsed from a file', $this->getPrettyName()), Exception\BrokerException::UNSUPPORTED); - } - - /** - * Returns the definition start line number in the file. - * - * @return null - */ - public function getStartLine() - { - return null; - } - - /** - * Returns the definition end line number in the file. - * - * @return null - */ - public function getEndLine() - { - return null; - } - - /** - * Returns the appropriate docblock definition. - * - * @return boolean - */ - public function getDocComment() - { - return false; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns the constant value. - * - * @return mixed - */ - public function getValue() - { - return null; - } - - /** - * Returns the part of the source code defining the constant value. - * - * @return string - */ - public function getValueDefinition() - { - return null; - } - - /** - * Returns the originaly provided value definition. - * - * @return string - */ - public function getOriginalValueDefinition() - { - return null; - } - - /** - * Returns if the constant is internal. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns if the constant is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return true; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return true; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->name; - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - return sprintf( - "Constant [ %s %s ] { %s }\n", - gettype(null), - $this->getName(), - null - ); - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return array(); - } - - /** - * Returns if the constant definition is valid. - * - * @return boolean - */ - public function isValid() - { - return false; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return ReflectionBase::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return ReflectionBase::exists($this, $key); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionElement.php b/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionElement.php deleted file mode 100644 index 344d62be16f..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionElement.php +++ /dev/null @@ -1,53 +0,0 @@ -reasons[] = $reason; - - return $this; - } - - /** - * Returns a list of reasons why this element's reflection is invalid. - * - * @return array - */ - public function getReasons() - { - return $this->reasons; - } - - /** - * Returns if there are any known reasons why this element's reflection is invalid. - * - * @return boolean - */ - public function hasReasons() - { - return !empty($this->reasons); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionFunction.php b/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionFunction.php deleted file mode 100644 index 475914b06ed..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Invalid/ReflectionFunction.php +++ /dev/null @@ -1,490 +0,0 @@ -name = ltrim($name, '\\'); - $this->broker = $broker; - $this->fileName = $fileName; - } - - /** - * Returns the name (FQN). - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - $pos = strrpos($this->name, '\\'); - return false === $pos ? $this->name : substr($this->name, $pos + 1); - } - - /** - * Returns the namespace name. - * - * @return string - */ - public function getNamespaceName() - { - $pos = strrpos($this->name, '\\'); - return false === $pos ? '' : substr($this->name, 0, $pos); - } - - /** - * Returns if the class is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return false !== strrpos($this->name, '\\'); - } - - /** - * Returns if the reflection object is internal. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns if the reflection object is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return true; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return true; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->name . '()'; - } - - /** - * Returns the PHP extension reflection. - * - * @return \TokenReflection\IReflectionExtension|null - */ - public function getExtension() - { - return null; - } - - /** - * Returns the PHP extension name. - * - * @return false - */ - public function getExtensionName() - { - return false; - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return null - */ - public function getFileName() - { - return $this->fileName; - } - - /** - * Returns a file reflection. - * - * @return \TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\RuntimeException If the file is not stored inside the broker - */ - public function getFileReflection() - { - throw new Exception\BrokerException($this->getBroker(), sprintf('Function was not parsed from a file', $this->getPrettyName()), Exception\BrokerException::UNSUPPORTED); - } - - /** - * Returns the appropriate source code part. - * - * @return string - */ - public function getSource() - { - return ''; - } - - /** - * Returns the start position in the file token stream. - * - * @return integer - */ - public function getStartPosition() - { - return -1; - } - - /** - * Returns the end position in the file token stream. - * - * @return integer - */ - public function getEndPosition() - { - return -1; - } - - /** - * Returns the definition start line number in the file. - * - * @return integer - */ - public function getStartLine() - { - return null; - } - - /** - * Returns the definition end line number in the file. - * - * @return integer - */ - public function getEndLine() - { - return null; - } - - /** - * Returns the appropriate docblock definition. - * - * @return boolean - */ - public function getDocComment() - { - return false; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return string|array|null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns all annotations. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns if the function/method is a closure. - * - * @return boolean - */ - public function isClosure() - { - return false; - } - - /** - * Returns if the function/method is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns if the function/method returns its value as reference. - * - * @return boolean - */ - public function returnsReference() - { - return false; - } - - /** - * Returns a function/method parameter. - * - * @param integer|string $parameter Parameter name or position - */ - public function getParameter($parameter) - { - if (is_numeric($parameter)) { - throw new Exception\RuntimeException(sprintf('There is no parameter at position "%d".', $parameter), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } else { - throw new Exception\RuntimeException(sprintf('There is no parameter "%s".', $parameter), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - } - - /** - * Returns function/method parameters. - * - * @return array - */ - public function getParameters(){ - return array(); - } - - /** - * Returns the number of parameters. - * - * @return integer - */ - public function getNumberOfParameters() - { - return 0; - } - - /** - * Returns the number of required parameters. - * - * @return integer - */ - public function getNumberOfRequiredParameters() - { - return 0; - } - - /** - * Returns static variables. - * - * @return array - */ - public function getStaticVariables() - { - return array(); - } - - /** - * Returns if the method is is disabled via the disable_functions directive. - * - * @return boolean - */ - public function isDisabled() - { - return false; - } - - /** - * Calls the function. - * - * @return mixed - */ - public function invoke() - { - return $this->invokeArgs(array()); - } - - /** - * Calls the function. - * - * @param array $args Function parameter values - * @return mixed - */ - public function invokeArgs(array $args) - { - throw new Exception\RuntimeException('Cannot invoke invalid functions', Exception\RuntimeException::UNSUPPORTED, $this); - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return array(); - } - - /** - * Returns the function/method as closure. - * - * @return \Closure - */ - public function getClosure() - { - throw new Exception\RuntimeException('Cannot invoke invalid functions', Exception\RuntimeException::UNSUPPORTED, $this); - } - - /** - * Returns the closure scope class. - * - * @return null - */ - public function getClosureScopeClass() - { - return null; - } - - /** - * Returns this pointer bound to closure. - * - * @return null - */ - public function getClosureThis() - { - return null; - } - - /** - * Returns if the function definition is valid. - * - * @return boolean - */ - public function isValid() - { - return false; - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - return sprintf( - "%sFunction [ function %s%s ] {\n @@ %s %d - %d\n}\n", - $this->getDocComment() ? $this->getDocComment() . "\n" : '', - $this->returnsReference() ? '&' : '', - $this->getName(), - $this->getFileName(), - $this->getStartLine(), - $this->getEndLine() - ); - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return ReflectionBase::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return ReflectionBase::exists($this, $key); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Php/IReflection.php b/apigen/libs/TokenReflection/TokenReflection/Php/IReflection.php deleted file mode 100644 index ce8353ed3b4..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Php/IReflection.php +++ /dev/null @@ -1,36 +0,0 @@ -broker = $broker; - } - - /** - * Returns the PHP extension reflection. - * - * @return \TokenReflection\Php\ReflectionExtension - */ - public function getExtension() - { - return ReflectionExtension::create(parent::getExtension(), $this->broker); - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns if the class is an exception or its descendant. - * - * @return boolean - */ - public function isException() - { - return 'Exception' === $this->getName() || $this->isSubclassOf('Exception'); - } - - /** - * Returns if objects of this class are cloneable. - * - * Introduced in PHP 5.4. - * - * @return boolean - * @see http://svn.php.net/viewvc/php/php-src/trunk/ext/reflection/php_reflection.c?revision=307971&view=markup#l4059 - */ - public function isCloneable() - { - if ($this->isInterface() || $this->isAbstract()) { - return false; - } - - $methods = $this->getMethods(); - return isset($methods['__clone']) ? $methods['__clone']->isPublic() : true; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return false; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns if the current class is a subclass of the given class. - * - * @param string|object $class Class name or reflection object - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If an invalid parameter was provided. - */ - public function isSubclassOf($class) - { - if (is_object($class)) { - if (!$class instanceof InternalReflectionClass && !$class instanceof IReflectionClass) { - throw new Exception\RuntimeException('Parameter must be a string or an instance of class reflection.', Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $class = $class->getName(); - } - - return in_array($class, $this->getParentClassNameList()); - } - - /** - * Returns parent class reflection. - * - * @return \TokenReflection\Php\ReflectionClass - */ - public function getParentClass() - { - $parent = parent::getParentClass(); - return $parent ? self::create($parent, $this->broker) : null; - } - - /** - * Returns the parent class name. - * - * @return string - */ - public function getParentClassName() - { - $parent = $this->getParentClass(); - return $parent ? $parent->getName() : null; - } - - /** - * Returns the parent classes reflections. - * - * @return array - */ - public function getParentClasses() - { - $broker = $this->broker; - return array_map(function($className) use ($broker) { - return $broker->getClass($className); - }, $this->getParentClassNameList()); - } - - /** - * Returns the parent classes names. - * - * @return array - */ - public function getParentClassNameList() - { - return class_parents($this->getName()); - } - - /** - * Returns if the class implements the given interface. - * - * @param string|object $interface Interface name or reflection object - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If the provided parameter is not an interface. - */ - public function implementsInterface($interface) - { - if (is_object($interface)) { - if (!$interface instanceof InternalReflectionClass && !$interface instanceof IReflectionClass) { - throw new Exception\RuntimeException('Parameter must be a string or an instance of class reflection.', Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $interfaceName = $interface->getName(); - - if (!$interface->isInterface()) { - throw new Exception\RuntimeException(sprintf('"%s" is not an interface.', $interfaceName), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - } else { - $reflection = $this->getBroker()->getClass($interface); - if (!$reflection->isInterface()) { - throw new Exception\RuntimeException(sprintf('"%s" is not an interface.', $interface), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $interfaceName = $interface; - } - - $interfaces = $this->getInterfaces(); - return isset($interfaces[$interfaceName]); - } - - /** - * Returns an array of interface reflections. - * - * @return array - */ - public function getInterfaces() - { - if (null === $this->interfaces) { - $broker = $this->broker; - $interfaceNames = $this->getInterfaceNames(); - - if (empty($interfaceNames)) { - $this->interfaces = array(); - } else { - $this->interfaces = array_combine($interfaceNames, array_map(function($interfaceName) use ($broker) { - return $broker->getClass($interfaceName); - }, $interfaceNames)); - } - } - - return $this->interfaces; - } - - /** - * Returns interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaces() - { - $parent = $this->getParentClass(); - return $parent ? array_diff_key($this->getInterfaces(), $parent->getInterfaces()) : $this->getInterfaces(); - } - - /** - * Returns names of interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaceNames() - { - return array_keys($this->getOwnInterfaces()); - } - - /** - * Returns class constructor reflection. - * - * @return \TokenReflection\Php\ReflectionClass|null - */ - public function getConstructor() - { - return ReflectionMethod::create(parent::getConstructor(), $this->broker); - } - - /** - * Returns class desctructor reflection. - * - * @return \TokenReflection\Php\ReflectionClass|null - */ - public function getDestructor() - { - foreach ($this->getMethods() as $method) { - if ($method->isDestructor()) { - return $method; - } - } - - return null; - } - - /** - * Returns a particular method reflection. - * - * @param string $name Method name - * @return \TokenReflection\Php\ReflectionMethod - * @throws \TokenReflection\Exception\RuntimeException If the requested method does not exist. - */ - public function getMethod($name) - { - foreach ($this->getMethods() as $method) { - if ($method->getName() === $name) { - return $method; - } - } - - throw new Exception\RuntimeException(sprintf('Method %s does not exist.', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns class methods. - * - * @param integer $filter Methods filter - * @return array - */ - public function getMethods($filter = null) - { - if (null === $this->methods) { - $broker = $this->broker; - $this->methods = array_map(function(InternalReflectionMethod $method) use ($broker) { - return ReflectionMethod::create($method, $broker); - }, parent::getMethods()); - } - - if (null === $filter) { - return $this->methods; - } - - return array_filter($this->methods, function(ReflectionMethod $method) use ($filter) { - return (bool) ($method->getModifiers() & $filter); - }); - } - - /** - * Returns if the class implements (and not its parents) the given method. - * - * @param string $name Method name - * @return boolean - */ - public function hasOwnMethod($name) - { - foreach ($this->getOwnMethods() as $method) { - if ($name === $method->getName()) { - return true; - } - } - - return false; - } - - /** - * Returns methods declared by this class, not its parents. - * - * @param integer $filter - * @return array - */ - public function getOwnMethods($filter = null) - { - $me = $this->getName(); - return array_filter($this->getMethods($filter), function(ReflectionMethod $method) use ($me) { - return $method->getDeclaringClass()->getName() === $me; - }); - } - - /** - * Returns if the class imports the given method from traits. - * - * @param string $name Method name - * @return boolean - * @todo Impossible with the current status of reflection - */ - public function hasTraitMethod($name) - { - return false; - } - - /** - * Returns method reflections imported from traits. - * - * @param integer $filter Methods filter - * @return array - * @todo Impossible with the current status of reflection - */ - public function getTraitMethods($filter = null) - { - return array(); - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \TokenReflection\ReflectionConstant - * @throws \TokenReflection\Exception\RuntimeException If the requested constant does not exist. - */ - public function getConstantReflection($name) - { - if ($this->hasConstant($name)) { - return new ReflectionConstant($name, $this->getConstant($name), $this->broker, $this); - } - - throw new Exception\RuntimeException(sprintf('Constant "%s" does not exist.', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns an array of constant reflections. - * - * @return array - */ - public function getConstantReflections() - { - if (null === $this->constants) { - $this->constants = array(); - foreach ($this->getConstants() as $name => $value) { - $this->constants[$name] = $this->getConstantReflection($name); - } - } - - return array_values($this->constants); - } - - /** - * Returns if the class (and not its parents) defines the given constant. - * - * @param string $name Constant name. - * @return boolean - */ - public function hasOwnConstant($name) - { - $constants = $this->getOwnConstants(); - return isset($constants[$name]); - } - - /** - * Returns constants declared by this class, not its parents. - * - * @return array - */ - public function getOwnConstants() - { - return array_diff_assoc($this->getConstants(), $this->getParentClass() ? $this->getParentClass()->getConstants() : array()); - } - - /** - * Returns an array of constant reflections defined by this class and not its parents. - * - * @return array - */ - public function getOwnConstantReflections() - { - $constants = array(); - foreach ($this->getOwnConstants() as $name => $value) { - $constants[] = $this->getConstantReflection($name); - } - return $constants; - } - - /** - * Returns a particular property reflection. - * - * @param string $name Property name - * @return \TokenReflection\Php\ReflectionProperty - * @throws \TokenReflection\Exception\RuntimeException If the requested property does not exist. - */ - public function getProperty($name) - { - foreach ($this->getProperties() as $property) { - if ($name === $property->getName()) { - return $property; - } - } - - throw new Exception\RuntimeException(sprintf('Property %s does not exist.', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns class properties. - * - * @param integer $filter Properties filter - * @return array - */ - public function getProperties($filter = null) - { - if (null === $this->properties) { - $broker = $this->broker; - $this->properties = array_map(function(InternalReflectionProperty $property) use ($broker) { - return ReflectionProperty::create($property, $broker); - }, parent::getProperties()); - } - - if (null === $filter) { - return $this->properties; - } - - return array_filter($this->properties, function(ReflectionProperty $property) use ($filter) { - return (bool) ($property->getModifiers() & $filter); - }); - } - - /** - * Returns if the class has (and not its parents) the given property. - * - * @param string $name Property name - * @return boolean - */ - public function hasOwnProperty($name) - { - foreach ($this->getOwnProperties() as $property) { - if ($name === $property->getName()) { - return true; - } - } - - return false; - } - - /** - * Returns properties declared by this class, not its parents. - * - * @param integer $filter - * @return array - */ - public function getOwnProperties($filter = null) - { - $me = $this->getName(); - return array_filter($this->getProperties($filter), function(ReflectionProperty $property) use ($me) { - return $property->getDeclaringClass()->getName() === $me; - }); - } - - /** - * Returns if the class imports the given property from traits. - * - * @param string $name Property name - * @return boolean - * @todo Impossible with the current status of reflection - */ - public function hasTraitProperty($name) - { - return false; - } - - /** - * Returns property reflections imported from traits. - * - * @param integer $filter Properties filter - * @return array - * @todo Impossible with the current status of reflection - */ - public function getTraitProperties($filter = null) - { - return array(); - } - - /** - * Returns static properties reflections. - * - * @return array - */ - public function getStaticProperties() - { - return $this->getProperties(InternalReflectionProperty::IS_STATIC); - } - - /** - * Returns reflections of direct subclasses. - * - * @return array - */ - public function getDirectSubclasses() - { - $that = $this->name; - return array_filter($this->getBroker()->getClasses(Broker\Backend::INTERNAL_CLASSES | Broker\Backend::TOKENIZED_CLASSES), function(IReflectionClass $class) use ($that) { - if (!$class->isSubclassOf($that)) { - return false; - } - - return null === $class->getParentClassName() || !$class->getParentClass()->isSubClassOf($that); - }); - } - - /** - * Returns names of direct subclasses. - * - * @return array - */ - public function getDirectSubclassNames() - { - return array_keys($this->getDirectSubclasses()); - } - - /** - * Returns reflections of indirect subclasses. - * - * @return array - */ - public function getIndirectSubclasses() - { - $that = $this->name; - return array_filter($this->getBroker()->getClasses(Broker\Backend::INTERNAL_CLASSES | Broker\Backend::TOKENIZED_CLASSES), function(IReflectionClass $class) use ($that) { - if (!$class->isSubclassOf($that)) { - return false; - } - - return null !== $class->getParentClassName() && $class->getParentClass()->isSubClassOf($that); - }); - } - - /** - * Returns names of indirect subclasses. - * - * @return array - */ - public function getIndirectSubclassNames() - { - return array_keys($this->getIndirectSubclasses()); - } - - /** - * Returns reflections of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementers() - { - if (!$this->isInterface()) { - return array(); - } - - $that = $this->name; - return array_filter($this->getBroker()->getClasses(Broker\Backend::INTERNAL_CLASSES | Broker\Backend::TOKENIZED_CLASSES), function(IReflectionClass $class) use ($that) { - if (!$class->implementsInterface($that)) { - return false; - } - - return null === $class->getParentClassName() || !$class->getParentClass()->implementsInterface($that); - }); - } - - /** - * Returns names of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementerNames() - { - return array_keys($this->getDirectImplementers()); - } - - /** - * Returns reflections of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementers() - { - if (!$this->isInterface()) { - return array(); - } - - $that = $this->name; - return array_filter($this->getBroker()->getClasses(Broker\Backend::INTERNAL_CLASSES | Broker\Backend::TOKENIZED_CLASSES), function(IReflectionClass $class) use ($that) { - if (!$class->implementsInterface($that)) { - return false; - } - - return null !== $class->getParentClassName() && $class->getParentClass()->implementsInterface($that); - }); - } - - /** - * Returns names of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementerNames() - { - return array_keys($this->getIndirectImplementers()); - } - - /** - * Returns if the class definition is complete. - * - * Internal classes always have the definition complete. - * - * @return boolean - */ - public function isComplete() - { - return true; - } - - /** - * Returns if the class definition is valid. - * - * Internal classes are always valid. - * - * @return boolean - */ - public function isValid() - { - return true; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return array(); - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return TokenReflection\ReflectionElement::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return TokenReflection\ReflectionElement::exists($this, $key); - } - - /** - * Returns traits used by this class. - * - * @return array - */ - public function getTraits() - { - return NATIVE_TRAITS ? parent::getTraits() : array(); - } - - /** - * Returns traits used by this class and not its parents. - * - * @return array - */ - public function getOwnTraits() - { - if (!NATIVE_TRAITS) { - return array(); - } - - $parent = $this->getParentClass(); - return $parent ? array_diff_key($this->getTraits(), $parent->getTraits()) : $this->getTraits(); - } - - /** - * Returns names of used traits. - * - * @return array - */ - public function getTraitNames() - { - return NATIVE_TRAITS ? parent::getTraitNames() : array(); - } - - /** - * Returns traits used by this class and not its parents. - * - * @return array - */ - public function getOwnTraitNames() - { - return array_keys($this->getOwnTraits()); - } - - /** - * Returns method aliases from traits. - * - * @return array - */ - public function getTraitAliases() - { - return NATIVE_TRAITS ? parent::getTraitAliases() : array(); - } - - /** - * Returns if the class is a trait. - * - * @return boolean - */ - public function isTrait() - { - return NATIVE_TRAITS && parent::isTrait(); - } - - /** - * Returns if the class uses a particular trait. - * - * @param \ReflectionClass|\TokenReflection\IReflectionClass|string $trait Trait reflection or name - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If an invalid parameter was provided. - */ - public function usesTrait($trait) - { - if (is_object($trait)) { - if (!$trait instanceof InternalReflectionClass && !$trait instanceof TokenReflection\IReflectionClass) { - throw new Exception\RuntimeException('Parameter must be a string or an instance of trait reflection.', Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $traitName = $trait->getName(); - - if (!$trait->isTrait()) { - throw new Exception\RuntimeException(sprintf('"%s" is not a trait.', $traitName), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - } else { - $reflection = $this->getBroker()->getClass($trait); - if (!$reflection->isTrait()) { - throw new Exception\RuntimeException(sprintf('"%s" is not a trait.', $trait), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $traitName = $trait; - } - - return in_array($traitName, $this->getTraitNames()); - } - - /** - * Creates a new class instance without using a constructor. - * - * @return object - * @throws \TokenReflection\Exception\RuntimeException If the class inherits from an internal class. - */ - public function newInstanceWithoutConstructor() - { - if ($this->isInternal()) { - throw new Exception\RuntimeException('Could not create an instance; only user defined classes can be instantiated.', Exception\RuntimeException::UNSUPPORTED, $this); - } - - foreach ($this->getParentClasses() as $parent) { - if ($parent->isInternal()) { - throw new Exception\RuntimeException('Could not create an instance; only user defined classes can be instantiated.', Exception\RuntimeException::UNSUPPORTED, $this); - } - } - - if (PHP_VERSION_ID >= 50400) { - return parent::newInstanceWithoutConstructor(); - } - - return unserialize(sprintf('O:%d:"%s":0:{}', strlen($this->getName()), $this->getName())); - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->getName(); - } - - /** - * Creates a reflection instance. - * - * @param \ReflectionClass $internalReflection Internal reflection instance - * @param \TokenReflection\Broker $broker Reflection broker instance - * @return \TokenReflection\Php\ReflectionClass - * @throws \TokenReflection\Exception\RuntimeException If an invalid internal reflection object was provided. - */ - public static function create(Reflector $internalReflection, Broker $broker) - { - if (!$internalReflection instanceof InternalReflectionClass) { - throw new Exception\RuntimeException('Invalid reflection instance provided, ReflectionClass expected.', Exception\RuntimeException::INVALID_ARGUMENT); - } - - return $broker->getClass($internalReflection->getName()); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionConstant.php b/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionConstant.php deleted file mode 100644 index e0de08fa562..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionConstant.php +++ /dev/null @@ -1,486 +0,0 @@ -name = $name; - $this->value = $value; - $this->broker = $broker; - - if (null !== $parent) { - $realParent = null; - - if (array_key_exists($name, $parent->getOwnConstants())) { - $realParent = $parent; - } - - if (null === $realParent) { - foreach ($parent->getParentClasses() as $grandParent) { - if (array_key_exists($name, $grandParent->getOwnConstants())) { - $realParent = $grandParent; - break; - } - } - } - - if (null === $realParent) { - foreach ($parent->getInterfaces() as $interface) { - if (array_key_exists($name, $interface->getOwnConstants())) { - $realParent = $interface; - break; - } - } - } - - if (null === $realParent) { - throw new Exception\RuntimeException('Could not determine constant real parent class.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $this->declaringClassName = $realParent->getName(); - $this->userDefined = $realParent->isUserDefined(); - } else { - if (!array_key_exists($name, get_defined_constants(false))) { - $this->userDefined = true; - } else { - $declared = get_defined_constants(true); - $this->userDefined = array_key_exists($name, $declared['user']); - } - } - } - - /** - * Returns the name. - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - $name = $this->getName(); - if (null !== $this->namespaceName && $this->namespaceName !== ReflectionNamespace::NO_NAMESPACE_NAME) { - $name = substr($name, strlen($this->namespaceName) + 1); - } - - return $name; - } - - /** - * Returns the declaring class reflection. - * - * @return \TokenReflection\IReflectionClass|null - */ - public function getDeclaringClass() - { - if (null === $this->declaringClassName) { - return null; - } - - return $this->getBroker()->getClass($this->declaringClassName); - } - - /** - * Returns the declaring class name. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->declaringClassName; - } - - /** - * Returns the namespace name. - * - * @return string - */ - public function getNamespaceName() - { - return $this->namespaceName === TokenReflection\ReflectionNamespace::NO_NAMESPACE_NAME ? '' : $this->namespaceName; - } - - /** - * Returns if the function/method is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return '' !== $this->getNamespaceName(); - } - - /** - * Returns the PHP extension reflection. - * - * @return null - */ - public function getExtension() - { - // @todo - return null; - } - - /** - * Returns the PHP extension name. - * - * @return boolean - */ - public function getExtensionName() - { - return false; - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return null - */ - public function getFileName() - { - return null; - } - - /** - * Returns the definition start line number in the file. - * - * @return null - */ - public function getStartLine() - { - return null; - } - - /** - * Returns the definition end line number in the file. - * - * @return null - */ - public function getEndLine() - { - return null; - } - - /** - * Returns the appropriate docblock definition. - * - * @return boolean - */ - public function getDocComment() - { - return false; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns the constant value. - * - * @return mixed - */ - public function getValue() - { - return $this->value; - } - - /** - * Returns the part of the source code defining the constant value. - * - * @return string - */ - public function getValueDefinition() - { - return var_export($this->value, true); - } - - /** - * Returns the originaly provided value definition. - * - * @return string - */ - public function getOriginalValueDefinition() - { - return token_get_all($this->getValueDefinition()); - } - - /** - * Returns if the constant is internal. - * - * @return boolean - */ - public function isInternal() - { - return !$this->userDefined; - } - - /** - * Returns if the constant is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return $this->userDefined; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return false; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return null === $this->declaringClassName ? $this->name : sprintf('%s::%s', $this->declaringClassName, $this->name); - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - return sprintf( - "Constant [ %s %s ] { %s }\n", - gettype($this->getValue()), - $this->getName(), - $this->getValue() - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string|object|null $class Class name, class instance or null - * @param string $constant Constant name - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $class, $constant, $return = false) - { - $className = is_object($class) ? get_class($class) : $class; - $constantName = $constant; - - if (null === $className) { - try { - $constant = $broker->getConstant($constantName); - } catch (Exception\BrokerException $e) { - throw new Exception\RuntimeException(sprintf('Constant %s does not exist.', $constantName), Exception\RuntimeException::DOES_NOT_EXIST); - } - } else { - $class = $broker->getClass($className); - if ($class instanceof Invalid\ReflectionClass) { - throw new Exception\RuntimeException('Class is invalid.', Exception\RuntimeException::UNSUPPORTED); - } elseif ($class instanceof Dummy\ReflectionClass) { - throw new Exception\RuntimeException(sprintf('Class %s does not exist.', $className), Exception\RuntimeException::DOES_NOT_EXIST); - } - $constant = $class->getConstantReflection($constantName); - } - - if ($return) { - return $constant->__toString(); - } - - echo $constant->__toString(); - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return array(); - } - - /** - * Returns if the constant definition is valid. - * - * Internal constants are always valid. - * - * @return boolean - */ - public function isValid() - { - return true; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return TokenReflection\ReflectionElement::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return TokenReflection\ReflectionElement::exists($this, $key); - } - - /** - * Creates a reflection instance. - * - * Not supported for constants since there is no internal constant reflection. - * - * @param \ReflectionClass $internalReflection Internal reflection instance - * @param \TokenReflection\Broker $broker Reflection broker instance - * @return null - */ - public static function create(Reflector $internalReflection, Broker $broker) - { - return null; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionExtension.php b/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionExtension.php deleted file mode 100644 index 20bbe7b96f9..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionExtension.php +++ /dev/null @@ -1,282 +0,0 @@ -broker = $broker; - } - - /** - * Returns if the constant is internal. - * - * @return boolean - */ - public function isInternal() - { - return true; - } - - /** - * Returns if the constant is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return false; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return false; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns a class reflection. - * - * @param string $name Class name - * @return \TokenReflection\IReflectionClass|null - */ - public function getClass($name) - { - $classes = $this->getClasses(); - return isset($classes[$name]) ? $classes[$name] : null; - } - - /** - * Returns classes defined by this extension. - * - * @return array - */ - public function getClasses() - { - if (null === $this->classes) { - $broker = $this->broker; - $this->classes = array_map(function($className) use ($broker) { - return $broker->getClass($className); - }, $this->getClassNames()); - } - - return $this->classes; - } - - /** - * Returns a constant value. - * - * @param string $name Constant name - * @return mixed|false - */ - public function getConstant($name) - { - $constants = $this->getConstants(); - return isset($constants[$name]) ? $constants[$name] : false; - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \TokenReflection\IReflectionConstant - */ - public function getConstantReflection($name) - { - $constants = $this->getConstantReflections(); - return isset($constants[$name]) ? $constants[$name] : null; - } - - /** - * Returns reflections of defined constants. - * - * @return array - */ - public function getConstantReflections() - { - if (null === $this->constants) { - $broker = $this->broker; - $this->constants = array_map(function($constantName) use ($broker) { - return $broker->getConstant($constantName); - }, array_keys($this->getConstants())); - } - - return $this->constants; - } - - /** - * Returns a function reflection. - * - * @param string $name Function name - * @return \TokenReflection\IReflectionFunction - */ - public function getFunction($name) - { - $functions = $this->getFunctions(); - return isset($functions[$name]) ? $functions[$name] : null; - } - - /** - * Returns functions defined by this extension. - * - * @return array - */ - public function getFunctions() - { - if (null === $this->functions) { - $broker = $this->broker; - $this->classes = array_map(function($functionName) use ($broker) { - return $broker->getFunction($functionName); - }, array_keys(parent::getFunctions())); - } - - return $this->functions; - } - - /** - * Returns names of functions defined by this extension. - * - * @return array - */ - public function getFunctionNames() - { - return array_keys($this->getFunctions()); - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->getName(); - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return TokenReflection\ReflectionElement::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return TokenReflection\ReflectionElement::exists($this, $key); - } - - /** - * Creates a reflection instance. - * - * @param \ReflectionClass $internalReflection Internal reflection instance - * @param \TokenReflection\Broker $broker Reflection broker instance - * @return \TokenReflection\Php\ReflectionExtension - * @throws \TokenReflection\Exception\RuntimeException If an invalid internal reflection object was provided. - */ - public static function create(Reflector $internalReflection, Broker $broker) - { - static $cache = array(); - - if (!$internalReflection instanceof InternalReflectionExtension) { - throw new Exception\RuntimeException('Invalid reflection instance provided, ReflectionExtension expected.', Exception\RuntimeException::INVALID_ARGUMENT); - } - - if (!isset($cache[$internalReflection->getName()])) { - $cache[$internalReflection->getName()] = new self($internalReflection->getName(), $broker); - } - - return $cache[$internalReflection->getName()]; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionFunction.php b/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionFunction.php deleted file mode 100644 index 63e47373496..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionFunction.php +++ /dev/null @@ -1,271 +0,0 @@ -broker = $broker; - } - - /** - * Returns the PHP extension reflection. - * - * @return \TokenReflection\IReflectionExtension - */ - public function getExtension() - { - return ReflectionExtension::create(parent::getExtension(), $this->broker); - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return false; - } - - /** - * Returns a particular parameter. - * - * @param integer|string $parameter Parameter name or position - * @return \TokenReflection\Php\ReflectionParameter - * @throws \TokenReflection\Exception\RuntimeException If there is no parameter of the given name. - * @throws \TokenReflection\Exception\RuntimeException If there is no parameter at the given position. - */ - public function getParameter($parameter) - { - $parameters = $this->getParameters(); - - if (is_numeric($parameter)) { - if (!isset($parameters[$parameter])) { - throw new Exception\RuntimeException(sprintf('There is no parameter at position "%d".', $parameter), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - return $parameters[$parameter]; - } else { - foreach ($parameters as $reflection) { - if ($reflection->getName() === $parameter) { - return $reflection; - } - } - - throw new Exception\RuntimeException(sprintf('There is no parameter "%s".', $parameter), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - } - - /** - * Returns function parameters. - * - * @return array - */ - public function getParameters() - { - if (null === $this->parameters) { - $broker = $this->broker; - $parent = $this; - $this->parameters = array_map(function(InternalReflectionParameter $parameter) use ($broker, $parent) { - return ReflectionParameter::create($parameter, $broker, $parent); - }, parent::getParameters()); - } - - return $this->parameters; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return array(); - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return TokenReflection\ReflectionElement::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return TokenReflection\ReflectionElement::exists($this, $key); - } - - /** - * Returns the function/method as closure. - * - * @return \Closure - */ - public function getClosure() - { - if (PHP_VERSION >= 50400) { - return parent::getClosure(); - } else { - $that = $this; - return function() use ($that) { - return $that->invokeArgs(func_get_args()); - }; - } - } - - /** - * Returns the closure scope class. - * - * @return string|null - */ - public function getClosureScopeClass() - { - return PHP_VERSION >= 50400 ? parent::getClosureScopeClass() : null; - } - - /** - * Returns this pointer bound to closure. - * - * @return null - */ - public function getClosureThis() - { - return PHP_VERSION >= 50400 ? parent::getClosureThis() : null; - } - - /** - * Returns if the function definition is valid. - * - * Internal functions are always valid. - * - * @return boolean - */ - public function isValid() - { - return true; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->getName() . '()'; - } - - /** - * Creates a reflection instance. - * - * @param \ReflectionClass $internalReflection Internal reflection instance - * @param \TokenReflection\Broker $broker Reflection broker instance - * @return \TokenReflection\Php\ReflectionFunction - * @throws \TokenReflection\Exception\RuntimeException If an invalid internal reflection object was provided. - */ - public static function create(Reflector $internalReflection, Broker $broker) - { - if (!$internalReflection instanceof InternalReflectionFunction) { - throw new Exception\RuntimeException('Invalid reflection instance provided, ReflectionFunction expected.', Exception\RuntimeException::INVALID_ARGUMENT); - } - - return $broker->getFunction($internalReflection->getName()); - } -} \ No newline at end of file diff --git a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionMethod.php b/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionMethod.php deleted file mode 100644 index 7a0c90ff71e..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionMethod.php +++ /dev/null @@ -1,385 +0,0 @@ -broker = $broker; - } - - /** - * Returns the declaring class reflection. - * - * @return \TokenReflection\IReflectionClass - */ - public function getDeclaringClass() - { - return ReflectionClass::create(parent::getDeclaringClass(), $this->broker); - } - - /** - * Returns the declaring class name. - * - * @return string - */ - public function getDeclaringClassName() - { - return $this->getDeclaringClass()->getName(); - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->getDeclaringClass()->getNamespaceAliases(); - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return false; - } - - /** - * Returns the method prototype. - * - * @return \TokenReflection\Php\ReflectionMethod - */ - public function getPrototype() - { - return self::create(parent::getPrototype(), $this->broker); - } - - /** - * Returns a particular parameter. - * - * @param integer|string $parameter Parameter name or position - * @return \TokenReflection\Php\ReflectionParameter - * @throws \TokenReflection\Exception\RuntimeException If there is no parameter of the given name. - * @throws \TokenReflection\Exception\RuntimeException If there is no parameter at the given position. - */ - public function getParameter($parameter) - { - $parameters = $this->getParameters(); - - if (is_numeric($parameter)) { - if (!isset($parameters[$parameter])) { - throw new Exception\RuntimeException(sprintf('There is no parameter at position "%d".', $parameter), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - return $parameters[$parameter]; - } else { - foreach ($parameters as $reflection) { - if ($reflection->getName() === $parameter) { - return $reflection; - } - } - - throw new Exception\RuntimeException(sprintf('There is no parameter "%s".', $parameter), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - } - - /** - * Returns function parameters. - * - * @return array - */ - public function getParameters() - { - if (null === $this->parameters) { - $broker = $this->broker; - $parent = $this; - $this->parameters = array_map(function(InternalReflectionParameter $parameter) use ($broker, $parent) { - return ReflectionParameter::create($parameter, $broker, $parent); - }, parent::getParameters()); - } - - return $this->parameters; - } - - /** - * Returns if the method is set accessible. - * - * @return boolean - */ - public function isAccessible() - { - return $this->accessible; - } - - /** - * Sets a method to be accessible or not. - * - * Introduced in PHP 5.3.2. Throws an exception if run on an older version. - * - * @param boolean $accessible - * @throws \TokenReflection\Exception\RuntimeException If run on PHP version < 5.3.2. - */ - public function setAccessible($accessible) - { - if (PHP_VERSION_ID < 50302) { - throw new Exception\RuntimeException(sprintf('Method setAccessible was introduced the internal reflection in PHP 5.3.2, you are using %s.', PHP_VERSION), Exception\RuntimeException::UNSUPPORTED, $this); - } - - $this->accessible = $accessible; - - parent::setAccessible($accessible); - } - - /** - * Shortcut for isPublic(), ... methods that allows or-ed modifiers. - * - * @param integer $filter Filter - * @return boolean - */ - public function is($filter = null) - { - return null === $filter || ($this->getModifiers() & $filter); - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return TokenReflection\ReflectionElement::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return TokenReflection\ReflectionElement::exists($this, $key); - } - - /** - * Returns the function/method as closure. - * - * @param object $object Object - * @return \Closure - */ - public function getClosure($object) - { - if (PHP_VERSION >= 50400) { - return parent::getClosure(); - } else { - $that = $this; - return function() use ($object, $that) { - return $that->invokeArgs($object, func_get_args()); - }; - } - } - - /** - * Returns the closure scope class. - * - * @return string|null - */ - public function getClosureScopeClass() - { - return PHP_VERSION >= 50400 ? parent::getClosureScopeClass() : null; - } - - /** - * Returns this pointer bound to closure. - * - * @return null - */ - public function getClosureThis() - { - return PHP_VERSION >= 50400 ? parent::getClosureThis() : null; - } - - /** - * Returns the original name when importing from a trait. - * - * @return string - */ - public function getOriginalName() - { - return null; - } - - /** - * Returns the original method when importing from a trait. - * - * @return null - */ - public function getOriginal() - { - return null; - } - - /** - * Returns the original modifiers value when importing from a trait. - * - * @return null - */ - public function getOriginalModifiers() - { - return null; - } - - /** - * Returns the defining trait. - * - * @return \TokenReflection\IReflectionClass|null - */ - public function getDeclaringTrait() - { - return null; - } - - /** - * Returns the declaring trait name. - * - * @return string|null - */ - public function getDeclaringTraitName() - { - return null; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return sprintf('%s::%s()', $this->getDeclaringClassName(), $this->getName()); - } - - /** - * Creates a reflection instance. - * - * @param \ReflectionClass $internalReflection Internal reflection instance - * @param \TokenReflection\Broker $broker Reflection broker instance - * @return \TokenReflection\Php\IReflection - * @throws \TokenReflection\Exception\RuntimeException If an invalid internal reflection object was provided. - */ - public static function create(Reflector $internalReflection, Broker $broker) - { - static $cache = array(); - - if (!$internalReflection instanceof InternalReflectionMethod) { - throw new Exception\RuntimeException('Invalid reflection instance provided, ReflectionMethod expected.', Exception\RuntimeException::INVALID_ARGUMENT); - } - - $key = $internalReflection->getDeclaringClass()->getName() . '::' . $internalReflection->getName(); - if (!isset($cache[$key])) { - $cache[$key] = new self($internalReflection->getDeclaringClass()->getName(), $internalReflection->getName(), $broker); - } - - return $cache[$key]; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionParameter.php b/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionParameter.php deleted file mode 100644 index 35cd7bba614..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionParameter.php +++ /dev/null @@ -1,368 +0,0 @@ -broker = $broker; - $this->userDefined = $parent->isUserDefined(); - } - - /** - * Returns the declaring class reflection. - * - * @return \TokenReflection\IReflectionClass - */ - public function getDeclaringClass() - { - $class = parent::getDeclaringClass(); - return $class ? ReflectionClass::create($class, $this->broker) : null; - } - - /** - * Returns the declaring class name. - * - * @return string|null - */ - public function getDeclaringClassName() - { - $class = parent::getDeclaringClass(); - return $class ? $class->getName() : null; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->getDeclaringFunction()->getNamespaceAliases(); - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return string - */ - public function getFileName() - { - return $this->getDeclaringFunction()->getFileName(); - } - - /** - * Returns the PHP extension reflection. - * - * @return \TokenReflection\Php\ReflectionExtension - */ - public function getExtension() - { - return $this->getDeclaringFunction()->getExtension(); - } - - /** - * Returns the PHP extension name. - * - * @return string|boolean - */ - public function getExtensionName() - { - $extension = $this->getExtension(); - return $extension ? $extension->getName() : false; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns the declaring function reflection. - * - * @return \TokenReflection\Php\ReflectionFunction|\TokenReflection\Php\ReflectionMethod - */ - public function getDeclaringFunction() - { - $class = $this->getDeclaringClass(); - $function = parent::getDeclaringFunction(); - - return $class ? $class->getMethod($function->getName()) : ReflectionFunction::create($function, $this->broker); - } - - /** - * Returns the declaring function name. - * - * @return string|null - */ - public function getDeclaringFunctionName() - { - $function = parent::getDeclaringFunction(); - return $function ? $function->getName() : $function; - } - - /** - * Returns the definition start line number in the file. - * - * @return null - */ - public function getStartLine() - { - return null; - } - - /** - * Returns the definition end line number in the file. - * - * @return null - */ - public function getEndLine() - { - return null; - } - - /** - * Returns the appropriate docblock definition. - * - * @return boolean - */ - public function getDocComment() - { - return false; - } - - /** - * Returns the part of the source code defining the paramter default value. - * - * @return string|null - */ - public function getDefaultValueDefinition() - { - $value = $this->getDefaultValue(); - return null === $value ? null : var_export($value, true); - } - - /** - * Returns if the parameter expects a callback. - * - * @return boolean - */ - public function isCallable() - { - return PHP_VERSION >= 50400 && parent::isCallable(); - } - - /** - * Returns the original type hint as defined in the source code. - * - * @return string|null - */ - public function getOriginalTypeHint() - { - return !$this->isArray() && !$this->isCallable() ? $this->getClass() : null; - } - - /** - * Returns the required class name of the value. - * - * @return string|null - */ - public function getClassName() - { - return $this->getClass() ? $this->getClass()->getName() : null; - } - - /** - * Returns if the parameter is internal. - * - * @return boolean - */ - public function isInternal() - { - return !$this->userDefined; - } - - /** - * Returns if the parameter is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return $this->userDefined; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return false; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Returns if the paramter value can be passed by value. - * - * @return boolean - */ - public function canBePassedByValue() - { - return method_exists($this, 'canBePassedByValue') ? parent::canBePassedByValue() : !$this->isPassedByReference(); - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return str_replace('()', '($' . $this->getName() . ')', $this->getDeclaringFunction()->getPrettyName()); - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return TokenReflection\ReflectionElement::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return TokenReflection\ReflectionElement::exists($this, $key); - } - - /** - * Creates a reflection instance. - * - * @param \ReflectionClass $internalReflection Internal reflection instance - * @param \TokenReflection\Broker $broker Reflection broker instance - * @return \TokenReflection\Php\ReflectionParameter - * @throws \TokenReflection\Exception\RuntimeException If an invalid internal reflection object was provided. - */ - public static function create(Reflector $internalReflection, Broker $broker) - { - static $cache = array(); - - if (!$internalReflection instanceof InternalReflectionParameter) { - throw new Exception\RuntimeException('Invalid reflection instance provided, ReflectionParameter expected.', Exception\RuntimeException::INVALID_ARGUMENT); - } - - $class = $internalReflection->getDeclaringClass(); - $function = $internalReflection->getDeclaringFunction(); - - $key = $class ? $class->getName() . '::' : ''; - $key .= $function->getName() . '(' . $internalReflection->getName() . ')'; - - if (!isset($cache[$key])) { - $cache[$key] = new self($class ? array($class->getName(), $function->getName()) : $function->getName(), $internalReflection->getName(), $broker, $function); - } - - return $cache[$key]; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionProperty.php b/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionProperty.php deleted file mode 100644 index fc0c192da06..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Php/ReflectionProperty.php +++ /dev/null @@ -1,348 +0,0 @@ -broker = $broker; - } - - /** - * Returns the declaring class reflection. - * - * @return \TokenReflection\IReflectionClass - */ - public function getDeclaringClass() - { - return ReflectionClass::create(parent::getDeclaringClass(), $this->broker); - } - - /** - * Returns the declaring class name. - * - * @return string - */ - public function getDeclaringClassName() - { - return $this->getDeclaringClass()->getName(); - } - - /** - * Returns the definition start line number in the file. - * - * @return null - */ - public function getStartLine() - { - return null; - } - - /** - * Returns the definition end line number in the file. - * - * @return null - */ - public function getEndLine() - { - return null; - } - - /** - * Returns the appropriate docblock definition. - * - * @return boolean - */ - public function getDocComment() - { - return false; - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - public function hasAnnotation($name) - { - return false; - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return null - */ - public function getAnnotation($name) - { - return null; - } - - /** - * Returns parsed docblock. - * - * @return array - */ - public function getAnnotations() - { - return array(); - } - - /** - * Returns the property default value. - * - * @return mixed - */ - public function getDefaultValue() - { - $values = $this->getDeclaringClass()->getDefaultProperties(); - return $values[$this->getName()]; - } - - /** - * Returns the part of the source code defining the property default value. - * - * @return string - */ - public function getDefaultValueDefinition() - { - $value = $this->getDefaultValue(); - return null === $value ? null : var_export($value, true); - } - - /** - * Returns if the property is internal. - * - * @return boolean - */ - public function isInternal() - { - return $this->getDeclaringClass()->isInternal(); - } - - /** - * Returns if the property is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return $this->getDeclaringClass()->isUserDefined(); - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return false; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return false; - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return array(); - } - - /** - * Returns the defining trait. - * - * @return \TokenReflection\IReflectionClass|null - */ - public function getDeclaringTrait() - { - return null; - } - - /** - * Returns the declaring trait name. - * - * @return string|null - */ - public function getDeclaringTraitName() - { - return null; - } - - /** - * Returns if the property is set accessible. - * - * @return boolean - */ - public function isAccessible() - { - return $this->accessible; - } - - /** - * Sets a property to be accessible or not. - * - * @param boolean $accessible If the property should be accessible. - */ - public function setAccessible($accessible) - { - $this->accessible = (bool) $accessible; - - parent::setAccessible($accessible); - } - - /** - * Returns the PHP extension reflection. - * - * @return \TokenReflection\Php\ReflectionExtension - */ - public function getExtension() - { - return $this->getDeclaringClass()->getExtension(); - } - - /** - * Returns the PHP extension name. - * - * @return string|boolean - */ - public function getExtensionName() - { - $extension = $this->getExtension(); - return $extension ? $extension->getName() : false; - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return string - */ - public function getFileName() - { - return $this->getDeclaringClass()->getFileName(); - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return sprintf('%s::$%s', $this->getDeclaringClassName(), $this->getName()); - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return TokenReflection\ReflectionElement::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return TokenReflection\ReflectionElement::exists($this, $key); - } - - /** - * Creates a reflection instance. - * - * @param \ReflectionClass $internalReflection Internal reflection instance - * @param \TokenReflection\Broker $broker Reflection broker instance - * @return \TokenReflection\Php\ReflectionProperty - * @throws \TokenReflection\Exception\RuntimeException If an invalid internal reflection object was provided. - */ - public static function create(Reflector $internalReflection, Broker $broker) - { - static $cache = array(); - - if (!$internalReflection instanceof InternalReflectionProperty) { - throw new Exception\RuntimeException('Invalid reflection instance provided, ReflectionProperty expected.', Exception\RuntimeException::INVALID_ARGUMENT); - } - - $key = $internalReflection->getDeclaringClass()->getName() . '::' . $internalReflection->getName(); - if (!isset($cache[$key])) { - $cache[$key] = new self($internalReflection->getDeclaringClass()->getName(), $internalReflection->getName(), $broker); - } - - return $cache[$key]; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionAnnotation.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionAnnotation.php deleted file mode 100644 index 6cd8def2fdc..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionAnnotation.php +++ /dev/null @@ -1,484 +0,0 @@ -reflection = $reflection; - $this->docComment = $docComment ?: false; - } - - /** - * Returns the docblock. - * - * @return string|boolean - */ - public function getDocComment() - { - return $this->docComment; - } - - /** - * Returns if the current docblock contains the requrested annotation. - * - * @param string $annotation Annotation name - * @return boolean - */ - public function hasAnnotation($annotation) - { - if (null === $this->annotations) { - $this->parse(); - } - - return isset($this->annotations[$annotation]); - } - - /** - * Returns a particular annotation value. - * - * @param string $annotation Annotation name - * @return string|array|null - */ - public function getAnnotation($annotation) - { - if (null === $this->annotations) { - $this->parse(); - } - - return isset($this->annotations[$annotation]) ? $this->annotations[$annotation] : null; - } - - /** - * Returns all parsed annotations. - * - * @return array - */ - public function getAnnotations() - { - if (null === $this->annotations) { - $this->parse(); - } - - return $this->annotations; - } - - /** - * Sets Docblock templates. - * - * @param array $templates Docblock templates - * @return \TokenReflection\ReflectionAnnotation - * @throws \TokenReflection\Exception\RuntimeException If an invalid annotation template was provided. - */ - public function setTemplates(array $templates) - { - foreach ($templates as $template) { - if (!$template instanceof ReflectionAnnotation) { - throw new Exception\RuntimeException( - sprintf( - 'All templates have to be instances of \\TokenReflection\\ReflectionAnnotation; %s given.', - is_object($template) ? get_class($template) : gettype($template) - ), - Exception\RuntimeException::INVALID_ARGUMENT, - $this->reflection - ); - } - } - - $this->templates = $templates; - - return $this; - } - - /** - * Parses reflection object documentation. - */ - private function parse() - { - $this->annotations = array(); - - if (false !== $this->docComment) { - // Parse docblock - $name = self::SHORT_DESCRIPTION; - $docblock = trim( - preg_replace( - array( - '~^' . preg_quote(ReflectionElement::DOCBLOCK_TEMPLATE_START, '~') . '~', - '~^' . preg_quote(ReflectionElement::DOCBLOCK_TEMPLATE_END, '~') . '$~', - '~^/\\*\\*~', - '~\\*/$~' - ), - '', - $this->docComment - ) - ); - foreach (explode("\n", $docblock) as $line) { - $line = preg_replace('~^\\*\\s?~', '', trim($line)); - - // End of short description - if ('' === $line && self::SHORT_DESCRIPTION === $name) { - $name = self::LONG_DESCRIPTION; - continue; - } - - // @annotation - if (preg_match('~^\\s*@([\\S]+)\\s*(.*)~', $line, $matches)) { - $name = $matches[1]; - $this->annotations[$name][] = $matches[2]; - continue; - } - - // Continuation - if (self::SHORT_DESCRIPTION === $name || self::LONG_DESCRIPTION === $name) { - if (!isset($this->annotations[$name])) { - $this->annotations[$name] = $line; - } else { - $this->annotations[$name] .= "\n" . $line; - } - } else { - $this->annotations[$name][count($this->annotations[$name]) - 1] .= "\n" . $line; - } - } - - array_walk_recursive($this->annotations, function(&$value) { - // {@*} is a placeholder for */ (phpDocumentor compatibility) - $value = str_replace('{@*}', '*/', $value); - $value = trim($value); - }); - } - - if ($this->reflection instanceof ReflectionElement) { - // Merge docblock templates - $this->mergeTemplates(); - - // Copy annotations if the @copydoc tag is present. - if (!empty($this->annotations['copydoc'])) { - $this->copyAnnotation(); - } - - // Process docblock inheritance for supported reflections - if ($this->reflection instanceof ReflectionClass || $this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty) { - $this->inheritAnnotations(); - } - } - } - - /** - * Copies annotations if the @copydoc tag is present. - * - * @throws \TokenReflection\Exception\RuntimeException When stuck in an infinite loop when resolving the @copydoc tag. - */ - private function copyAnnotation() - { - self::$copydocStack[] = $this->reflection; - $broker = $this->reflection->getBroker(); - - $parentNames = $this->annotations['copydoc']; - unset($this->annotations['copydoc']); - - foreach ($parentNames as $parentName) { - try { - if ($this->reflection instanceof ReflectionClass) { - $parent = $broker->getClass($parentName); - if ($parent instanceof Dummy\ReflectionClass) { - // The class to copy from is not usable - return; - } - } elseif ($this->reflection instanceof ReflectionFunction) { - $parent = $broker->getFunction(rtrim($parentName, '()')); - } elseif ($this->reflection instanceof ReflectionConstant && null === $this->reflection->getDeclaringClassName()) { - $parent = $broker->getConstant($parentName); - } elseif ($this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty || $this->reflection instanceof ReflectionConstant) { - if (false !== strpos($parentName, '::')) { - list($className, $parentName) = explode('::', $parentName, 2); - $class = $broker->getClass($className); - } else { - $class = $this->reflection->getDeclaringClass(); - } - - if ($class instanceof Dummy\ReflectionClass) { - // The source element class is not usable - return; - } - - if ($this->reflection instanceof ReflectionMethod) { - $parent = $class->getMethod(rtrim($parentName, '()')); - } elseif ($this->reflection instanceof ReflectionConstant) { - $parent = $class->getConstantReflection($parentName); - } else { - $parent = $class->getProperty(ltrim($parentName, '$')); - } - } - - if (!empty($parent)) { - // Don't get into an infinite recursion loop - if (in_array($parent, self::$copydocStack, true)) { - throw new Exception\RuntimeException('Infinite loop detected when copying annotations using the @copydoc tag.', Exception\RuntimeException::INVALID_ARGUMENT, $this->reflection); - } - - self::$copydocStack[] = $parent; - - // We can get into an infinite loop here (e.g. when two methods @copydoc from each other) - foreach ($parent->getAnnotations() as $name => $value) { - // Add annotations that are not already present - if (empty($this->annotations[$name])) { - $this->annotations[$name] = $value; - } - } - - array_pop(self::$copydocStack); - } - } catch (Exception\BaseException $e) { - // Ignoring links to non existent elements, ... - } - } - - array_pop(self::$copydocStack); - } - - /** - * Merges templates with the current docblock. - */ - private function mergeTemplates() - { - foreach ($this->templates as $index => $template) { - if (0 === $index && $template->getDocComment() === $this->docComment) { - continue; - } - - foreach ($template->getAnnotations() as $name => $value) { - if ($name === self::LONG_DESCRIPTION) { - // Long description - if (isset($this->annotations[self::LONG_DESCRIPTION])) { - $this->annotations[self::LONG_DESCRIPTION] = $value . "\n" . $this->annotations[self::LONG_DESCRIPTION]; - } else { - $this->annotations[self::LONG_DESCRIPTION] = $value; - } - } elseif ($name !== self::SHORT_DESCRIPTION) { - // Tags; short description is not inherited - if (isset($this->annotations[$name])) { - $this->annotations[$name] = array_merge($this->annotations[$name], $value); - } else { - $this->annotations[$name] = $value; - } - } - } - } - } - - /** - * Inherits annotations from parent classes/methods/properties if needed. - * - * @throws \TokenReflection\Exception\RuntimeException If unsupported reflection was used. - */ - private function inheritAnnotations() - { - if ($this->reflection instanceof ReflectionClass) { - $declaringClass = $this->reflection; - } elseif ($this->reflection instanceof ReflectionMethod || $this->reflection instanceof ReflectionProperty) { - $declaringClass = $this->reflection->getDeclaringClass(); - } - - $parents = array_filter(array_merge(array($declaringClass->getParentClass()), $declaringClass->getOwnInterfaces()), function($class) { - return $class instanceof ReflectionClass; - }); - - // In case of properties and methods, look for a property/method of the same name and return - // and array of such members. - $parentDefinitions = array(); - if ($this->reflection instanceof ReflectionProperty) { - $name = $this->reflection->getName(); - foreach ($parents as $parent) { - if ($parent->hasProperty($name)) { - $parentDefinitions[] = $parent->getProperty($name); - } - } - - $parents = $parentDefinitions; - } elseif ($this->reflection instanceof ReflectionMethod) { - $name = $this->reflection->getName(); - foreach ($parents as $parent) { - if ($parent->hasMethod($name)) { - $parentDefinitions[] = $parent->getMethod($name); - } - } - - $parents = $parentDefinitions; - } - - if (false === $this->docComment) { - // Inherit the entire docblock - foreach ($parents as $parent) { - $annotations = $parent->getAnnotations(); - if (!empty($annotations)) { - $this->annotations = $annotations; - break; - } - } - } else { - if (isset($this->annotations[self::LONG_DESCRIPTION]) && false !== stripos($this->annotations[self::LONG_DESCRIPTION], '{@inheritdoc}')) { - // Inherit long description - foreach ($parents as $parent) { - if ($parent->hasAnnotation(self::LONG_DESCRIPTION)) { - $this->annotations[self::LONG_DESCRIPTION] = str_ireplace( - '{@inheritdoc}', - $parent->getAnnotation(self::LONG_DESCRIPTION), - $this->annotations[self::LONG_DESCRIPTION] - ); - break; - } - } - - $this->annotations[self::LONG_DESCRIPTION] = str_ireplace('{@inheritdoc}', '', $this->annotations[self::LONG_DESCRIPTION]); - } - if (isset($this->annotations[self::SHORT_DESCRIPTION]) && false !== stripos($this->annotations[self::SHORT_DESCRIPTION], '{@inheritdoc}')) { - // Inherit short description - foreach ($parents as $parent) { - if ($parent->hasAnnotation(self::SHORT_DESCRIPTION)) { - $this->annotations[self::SHORT_DESCRIPTION] = str_ireplace( - '{@inheritdoc}', - $parent->getAnnotation(self::SHORT_DESCRIPTION), - $this->annotations[self::SHORT_DESCRIPTION] - ); - break; - } - } - - $this->annotations[self::SHORT_DESCRIPTION] = str_ireplace('{@inheritdoc}', '', $this->annotations[self::SHORT_DESCRIPTION]); - } - } - - // In case of properties check if we need and can inherit the data type - if ($this->reflection instanceof ReflectionProperty && empty($this->annotations['var'])) { - foreach ($parents as $parent) { - if ($parent->hasAnnotation('var')) { - $this->annotations['var'] = $parent->getAnnotation('var'); - break; - } - } - } - - if ($this->reflection instanceof ReflectionMethod) { - if (0 !== $this->reflection->getNumberOfParameters() && (empty($this->annotations['param']) || count($this->annotations['param']) < $this->reflection->getNumberOfParameters())) { - // In case of methods check if we need and can inherit parameter descriptions - $params = isset($this->annotations['param']) ? $this->annotations['param'] : array(); - $complete = false; - foreach ($parents as $parent) { - if ($parent->hasAnnotation('param')) { - $parentParams = array_slice($parent->getAnnotation('param'), count($params)); - - while (!empty($parentParams) && !$complete) { - array_push($params, array_shift($parentParams)); - - if (count($params) === $this->reflection->getNumberOfParameters()) { - $complete = true; - } - } - } - - if ($complete) { - break; - } - } - - if (!empty($params)) { - $this->annotations['param'] = $params; - } - } - - // And check if we need and can inherit the return and throws value - foreach (array('return', 'throws') as $paramName) { - if (!isset($this->annotations[$paramName])) { - foreach ($parents as $parent) { - if ($parent->hasAnnotation($paramName)) { - $this->annotations[$paramName] = $parent->getAnnotation($paramName); - break; - } - } - } - } - } - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionBase.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionBase.php deleted file mode 100644 index c7b15b0a399..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionBase.php +++ /dev/null @@ -1,273 +0,0 @@ -broker = $broker; - - $this->parseStream($tokenStream, $parent); - } - - /** - * Parses the token substream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - */ - abstract protected function parseStream(Stream $tokenStream, IReflection $parent = null); - - /** - * Returns the name (FQN). - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the appropriate docblock definition. - * - * @return string|boolean - */ - public function getDocComment() - { - return $this->docComment->getDocComment(); - } - - /** - * Checks if there is a particular annotation. - * - * @param string $name Annotation name - * @return boolean - */ - final public function hasAnnotation($name) - { - return $this->docComment->hasAnnotation($name); - } - - /** - * Returns a particular annotation value. - * - * @param string $name Annotation name - * @return string|array|null - */ - final public function getAnnotation($name) - { - return $this->docComment->getAnnotation($name); - } - - /** - * Returns all annotations. - * - * @return array - */ - final public function getAnnotations() - { - return $this->docComment->getAnnotations(); - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Returns if the reflection object is internal. - * - * Always returns false - everything is user defined. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns if the reflection object is user defined. - * - * Always returns true - everything is user defined. - * - * @return boolean - */ - public function isUserDefined() - { - return true; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return true; - } - - /** - * Returns if the reflection subject is deprecated. - * - * @return boolean - */ - public function isDeprecated() - { - return $this->hasAnnotation('deprecated'); - } - - /** - * Returns the appropriate source code part. - * - * @return string - */ - abstract public function getSource(); - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->name; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return self::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return self::exists($this, $key); - } - - /** - * Magic __get method helper. - * - * @param \TokenReflection\IReflection $object Reflection object - * @param string $key Variable name - * @return mixed - * @throws \TokenReflection\Exception\RuntimeException If the requested parameter does not exist. - */ - final public static function get(IReflection $object, $key) - { - if (!empty($key)) { - $className = get_class($object); - if (!isset(self::$methodCache[$className])) { - self::$methodCache[$className] = array_flip(get_class_methods($className)); - } - - $methods = self::$methodCache[$className]; - $key2 = ucfirst($key); - if (isset($methods['get' . $key2])) { - return $object->{'get' . $key2}(); - } elseif (isset($methods['is' . $key2])) { - return $object->{'is' . $key2}(); - } - } - - throw new Exception\RuntimeException(sprintf('Cannot read property "%s".', $key), Exception\RuntimeException::DOES_NOT_EXIST); - } - - /** - * Magic __isset method helper. - * - * @param \TokenReflection\IReflection $object Reflection object - * @param string $key Variable name - * @return boolean - */ - final public static function exists(IReflection $object, $key) - { - try { - self::get($object, $key); - return true; - } catch (RuntimeException $e) { - return false; - } - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionClass.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionClass.php deleted file mode 100644 index 088ebca0b0b..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionClass.php +++ /dev/null @@ -1,1986 +0,0 @@ -::] => array( - * array(, [])|null - * [, ...] - * ) - * - * @var array - */ - private $traitImports = array(); - - /** - * Stores if the class definition is complete. - * - * @var array - */ - private $methods = array(); - - /** - * Constant reflections. - * - * @var array - */ - private $constants = array(); - - /** - * Properties reflections. - * - * @var array - */ - private $properties = array(); - - /** - * Stores if the class definition is complete. - * - * @var boolean - */ - private $definitionComplete = false; - - /** - * Imported namespace/class aliases. - * - * @var array - */ - private $aliases = array(); - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - $name = $this->getName(); - if ($this->namespaceName !== ReflectionNamespace::NO_NAMESPACE_NAME) { - $name = substr($name, strlen($this->namespaceName) + 1); - } - - return $name; - } - - /** - * Returns the namespace name. - * - * @return string - */ - public function getNamespaceName() - { - return $this->namespaceName === ReflectionNamespace::NO_NAMESPACE_NAME ? '' : $this->namespaceName; - } - - /** - * Returns if the class is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return null !== $this->namespaceName && ReflectionNamespace::NO_NAMESPACE_NAME !== $this->namespaceName; - } - - /** - * Returns modifiers. - * - * @return array - */ - public function getModifiers() - { - if (false === $this->modifiersComplete) { - if (($this->modifiers & InternalReflectionClass::IS_EXPLICIT_ABSTRACT) && !($this->modifiers & InternalReflectionClass::IS_IMPLICIT_ABSTRACT)) { - foreach ($this->getMethods() as $reflectionMethod) { - if ($reflectionMethod->isAbstract()) { - $this->modifiers |= InternalReflectionClass::IS_IMPLICIT_ABSTRACT; - } - } - - if (!empty($this->interfaces)) { - $this->modifiers |= InternalReflectionClass::IS_IMPLICIT_ABSTRACT; - } - } - - if (!empty($this->interfaces)) { - $this->modifiers |= self::IMPLEMENTS_INTERFACES; - } - - if ($this->isInterface() && !empty($this->methods)) { - $this->modifiers |= InternalReflectionClass::IS_IMPLICIT_ABSTRACT; - } - - if (!empty($this->traits)) { - $this->modifiers |= self::IMPLEMENTS_TRAITS; - } - - $this->modifiersComplete = null === $this->parentClassName || $this->getParentClass()->isComplete(); - - if ($this->modifiersComplete) { - foreach ($this->getInterfaces() as $interface) { - if (!$interface->isComplete()) { - $this->modifiersComplete = false; - break; - } - } - } - if ($this->modifiersComplete) { - foreach ($this->getTraits() as $trait) { - if (!$trait->isComplete()) { - $this->modifiersComplete = false; - break; - } - } - } - } - - return $this->modifiers; - } - - /** - * Returns if the class is abstract. - * - * @return boolean - */ - public function isAbstract() - { - if ($this->modifiers & InternalReflectionClass::IS_EXPLICIT_ABSTRACT) { - return true; - } elseif ($this->isInterface() && !empty($this->methods)) { - return true; - } - - return false; - } - - /** - * Returns if the class is final. - * - * @return boolean - */ - public function isFinal() - { - return (bool) ($this->modifiers & InternalReflectionClass::IS_FINAL); - } - - /** - * Returns if the class is an interface. - * - * @return boolean - */ - public function isInterface() - { - return (bool) ($this->modifiers & self::IS_INTERFACE); - } - - /** - * Returns if the class is an exception or its descendant. - * - * @return boolean - */ - public function isException() - { - return 'Exception' === $this->name || $this->isSubclassOf('Exception'); - } - - /** - * Returns if it is possible to create an instance of this class. - * - * @return boolean - */ - public function isInstantiable() - { - if ($this->isInterface() || $this->isAbstract()) { - return false; - } - - if (null === ($constructor = $this->getConstructor())) { - return true; - } - - return $constructor->isPublic(); - } - - /** - * Returns if objects of this class are cloneable. - * - * Introduced in PHP 5.4. - * - * @return boolean - * @see http://svn.php.net/viewvc/php/php-src/trunk/ext/reflection/php_reflection.c?revision=307971&view=markup#l4059 - */ - public function isCloneable() - { - if ($this->isInterface() || $this->isAbstract()) { - return false; - } - - if ($this->hasMethod('__clone')) { - return $this->getMethod('__clone')->isPublic(); - } - - return true; - } - - /** - * Returns if the class is iterateable. - * - * Returns true if the class implements the Traversable interface. - * - * @return boolean - * @todo traits - */ - public function isIterateable() - { - return $this->implementsInterface('Traversable'); - } - - /** - * Returns if the current class is a subclass of the given class. - * - * @param string|object $class Class name or reflection object - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If the provided parameter is not a reflection class instance. - */ - public function isSubclassOf($class) - { - if (is_object($class)) { - if ($class instanceof InternalReflectionClass || $class instanceof IReflectionClass) { - $class = $class->getName(); - } else { - $class = get_class($class); - } - } - - if ($class === $this->parentClassName) { - return true; - } - - $parent = $this->getParentClass(); - return false === $parent ? false : $parent->isSubclassOf($class); - } - - /** - * Returns the parent class reflection. - * - * @return \TokenReflection\ReflectionClass|boolean - */ - public function getParentClass() - { - $className = $this->getParentClassName(); - if (null === $className) { - return false; - } - - return $this->getBroker()->getClass($className); - } - - /** - * Returns the parent class name. - * - * @return string|null - */ - public function getParentClassName() - { - return $this->parentClassName; - } - - /** - * Returns the parent classes reflections. - * - * @return array - */ - public function getParentClasses() - { - $parent = $this->getParentClass(); - if (false === $parent) { - return array(); - } - - return array_merge(array($parent->getName() => $parent), $parent->getParentClasses()); - } - - /** - * Returns the parent classes names. - * - * @return array - */ - public function getParentClassNameList() - { - $parent = $this->getParentClass(); - if (false === $parent) { - return array(); - } - - return array_merge(array($parent->getName()), $parent->getParentClassNameList()); - } - - /** - * Returns if the class implements the given interface. - * - * @param string|object $interface Interface name or reflection object - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If the provided parameter is not an interface. - */ - public function implementsInterface($interface) - { - if (is_object($interface)) { - if (!$interface instanceof InternalReflectionClass && !$interface instanceof IReflectionClass) { - throw new Exception\RuntimeException(sprintf('Parameter must be a string or an instance of class reflection, "%s" provided.', get_class($interface)), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - if (!$interface->isInterface()) { - throw new Exception\RuntimeException(sprintf('"%s" is not an interface.', $interfaceName), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $interfaceName = $interface->getName(); - } else { - $interfaceName = $interface; - } - - return in_array($interfaceName, $this->getInterfaceNames()); - } - - /** - * Returns interface reflections. - * - * @return array - */ - public function getInterfaces() - { - $interfaceNames = $this->getInterfaceNames(); - if (empty($interfaceNames)) { - return array(); - } - - $broker = $this->getBroker(); - return array_combine($interfaceNames, array_map(function($interfaceName) use ($broker) { - return $broker->getClass($interfaceName); - }, $interfaceNames)); - } - - /** - * Returns interface names. - * - * @return array - */ - public function getInterfaceNames() - { - $parentClass = $this->getParentClass(); - - $names = false !== $parentClass ? array_reverse(array_flip($parentClass->getInterfaceNames())) : array(); - foreach ($this->interfaces as $interfaceName) { - $names[$interfaceName] = true; - foreach (array_reverse($this->getBroker()->getClass($interfaceName)->getInterfaceNames()) as $parentInterfaceName) { - $names[$parentInterfaceName] = true; - } - } - - return array_keys($names); - } - - /** - * Returns reflections of interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaces() - { - $interfaceNames = $this->getOwnInterfaceNames(); - if (empty($interfaceNames)) { - return array(); - } - - $broker = $this->getBroker(); - return array_combine($interfaceNames, array_map(function($interfaceName) use ($broker) { - return $broker->getClass($interfaceName); - }, $interfaceNames)); - } - - /** - * Returns names of interfaces implemented by this class, not its parents. - * - * @return array - */ - public function getOwnInterfaceNames() - { - return $this->interfaces; - } - - /** - * Returns the class constructor reflection. - * - * @return \TokenReflection\ReflectionMethod|null - */ - public function getConstructor() - { - foreach ($this->getMethods() as $method) { - if ($method->isConstructor()) { - return $method; - } - } - - return null; - } - - /** - * Returns the class destructor reflection. - * - * @return \TokenReflection\ReflectionMethod|null - */ - public function getDestructor() - { - foreach ($this->getMethods() as $method) { - if ($method->isDestructor()) { - return $method; - } - } - - return null; - } - - /** - * Returns if the class implements the given method. - * - * @param string $name Method name - * @return boolean - */ - public function hasMethod($name) - { - foreach ($this->getMethods() as $method) { - if ($name === $method->getName()) { - return true; - } - } - - return false; - } - - /** - * Returns a method reflection. - * - * @param string $name Method name - * @return \TokenReflection\ReflectionMethod - * @throws \TokenReflection\Exception\RuntimeException If the requested method does not exist. - */ - public function getMethod($name) - { - if (isset($this->methods[$name])) { - return $this->methods[$name]; - } - - foreach ($this->getMethods() as $method) { - if ($name === $method->getName()) { - return $method; - } - } - - throw new Exception\RuntimeException(sprintf('There is no method "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns method reflections. - * - * @param integer $filter Methods filter - * @return array - */ - public function getMethods($filter = null) - { - $methods = $this->methods; - - foreach ($this->getTraitMethods() as $traitMethod) { - if (!isset($methods[$traitMethod->getName()])) { - $methods[$traitMethod->getName()] = $traitMethod; - } - } - - if (null !== $this->parentClassName) { - foreach ($this->getParentClass()->getMethods(null) as $parentMethod) { - if (!isset($methods[$parentMethod->getName()])) { - $methods[$parentMethod->getName()] = $parentMethod; - } - } - } - foreach ($this->getOwnInterfaces() as $interface) { - foreach ($interface->getMethods(null) as $parentMethod) { - if (!isset($methods[$parentMethod->getName()])) { - $methods[$parentMethod->getName()] = $parentMethod; - } - } - } - - if (null !== $filter) { - $methods = array_filter($methods, function(IReflectionMethod $method) use ($filter) { - return $method->is($filter); - }); - } - - return array_values($methods); - } - - /** - * Returns if the class implements (and not its parents) the given method. - * - * @param string $name Method name - * @return boolean - */ - public function hasOwnMethod($name) - { - return isset($this->methods[$name]); - } - - /** - * Returns reflections of methods declared by this class, not its parents. - * - * @param integer $filter Methods filter - * @return array - */ - public function getOwnMethods($filter = null) - { - $methods = $this->methods; - - if (null !== $filter) { - $methods = array_filter($methods, function(ReflectionMethod $method) use ($filter) { - return $method->is($filter); - }); - } - - return array_values($methods); - } - - /** - * Returns if the class imports the given method from traits. - * - * @param string $name Method name - * @return boolean - */ - public function hasTraitMethod($name) - { - if (isset($this->methods[$name])) { - return false; - } - - foreach ($this->getOwnTraits() as $trait) { - if ($trait->hasMethod($name)) { - return true; - } - } - - return false; - } - - /** - * Returns reflections of method imported from traits. - * - * @param integer $filter Methods filter - * @return array - * @throws \TokenReflection\Exception\RuntimeException If trait method was already imported. - */ - public function getTraitMethods($filter = null) - { - $methods = array(); - - foreach ($this->getOwnTraits() as $trait) { - $traitName = $trait->getName(); - foreach ($trait->getMethods(null) as $traitMethod) { - $methodName = $traitMethod->getName(); - - $imports = array(); - if (isset($this->traitImports[$traitName . '::' . $methodName])) { - $imports = $this->traitImports[$traitName . '::' . $methodName]; - } - if (isset($this->traitImports[$methodName])) { - $imports = empty($imports) ? $this->traitImports[$methodName] : array_merge($imports, $this->traitImports[$methodName]); - } - - foreach ($imports as $import) { - if (null !== $import) { - list($newName, $accessLevel) = $import; - - if ('' === $newName) { - $newName = $methodName; - $imports[] = null; - } - - if (!isset($this->methods[$newName])) { - if (isset($methods[$newName])) { - throw new Exception\RuntimeException(sprintf('Trait method "%s" was already imported.', $newName), Exception\RuntimeException::ALREADY_EXISTS, $this); - } - - $methods[$newName] = $traitMethod->alias($this, $newName, $accessLevel); - } - } - } - - if (!in_array(null, $imports)) { - if (!isset($this->methods[$methodName])) { - if (isset($methods[$methodName])) { - throw new Exception\RuntimeException(sprintf('Trait method "%s" was already imported.', $methodName), Exception\RuntimeException::ALREADY_EXISTS, $this); - } - - $methods[$methodName] = $traitMethod->alias($this); - } - } - } - } - - if (null !== $filter) { - $methods = array_filter($methods, function(IReflectionMethod $method) use ($filter) { - return (bool) ($method->getModifiers() & $filter); - }); - } - - return array_values($methods); - } - - /** - * Returns if the class defines the given constant. - * - * @param string $name Constant name. - * @return boolean - */ - public function hasConstant($name) - { - if (isset($this->constants[$name])) { - return true; - } - - foreach ($this->getConstantReflections() as $constant) { - if ($name === $constant->getName()) { - return true; - } - } - - return false; - } - - /** - * Returns a constant value. - * - * @param string $name Constant name - * @return mixed|false - */ - public function getConstant($name) - { - try { - return $this->getConstantReflection($name)->getValue(); - } catch (Exception\BaseException $e) { - return false; - } - } - - /** - * Returns a constant reflection. - * - * @param string $name Constant name - * @return \TokenReflection\ReflectionConstant - * @throws \TokenReflection\Exception\RuntimeException If the requested constant does not exist. - */ - public function getConstantReflection($name) - { - if (isset($this->constants[$name])) { - return $this->constants[$name]; - } - - foreach ($this->getConstantReflections() as $constant) { - if ($name === $constant->getName()) { - return $constant; - } - } - - throw new Exception\RuntimeException(sprintf('There is no constant "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns constant values. - * - * @return array - */ - public function getConstants() - { - $constants = array(); - foreach ($this->getConstantReflections() as $constant) { - $constants[$constant->getName()] = $constant->getValue(); - } - return $constants; - } - - /** - * Returns constant reflections. - * - * @return array - */ - public function getConstantReflections() - { - if (null === $this->parentClassName && empty($this->interfaces)) { - return array_values($this->constants); - } else { - $reflections = array_values($this->constants); - - if (null !== $this->parentClassName) { - $reflections = array_merge($reflections, $this->getParentClass()->getConstantReflections()); - } - foreach ($this->getOwnInterfaces() as $interface) { - $reflections = array_merge($reflections, $interface->getConstantReflections()); - } - - return $reflections; - } - } - - /** - * Returns if the class (and not its parents) defines the given constant. - * - * @param string $name Constant name. - * @return boolean - */ - public function hasOwnConstant($name) - { - return isset($this->constants[$name]); - } - - /** - * Returns constants declared by this class, not by its parents. - * - * @return array - */ - public function getOwnConstants() - { - return array_map(function(ReflectionConstant $constant) { - return $constant->getValue(); - }, $this->constants); - } - - /** - * Returns reflections of constants declared by this class, not by its parents. - * - * @return array - */ - public function getOwnConstantReflections() - { - return array_values($this->constants); - } - - /** - * Returns if the class defines the given property. - * - * @param string $name Property name - * @return boolean - */ - public function hasProperty($name) - { - foreach ($this->getProperties() as $property) { - if ($name === $property->getName()) { - return true; - } - } - - return false; - } - - /** - * Return a property reflection. - * - * @param string $name Property name - * @return \TokenReflection\ReflectionProperty - * @throws \TokenReflection\Exception\RuntimeException If the requested property does not exist. - */ - public function getProperty($name) - { - if (isset($this->properties[$name])) { - return $this->properties[$name]; - } - - foreach ($this->getProperties() as $property) { - if ($name === $property->getName()) { - return $property; - } - } - - throw new Exception\RuntimeException(sprintf('There is no property "%s".', $name, $this->name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns property reflections. - * - * @param integer $filter Properties filter - * @return array - */ - public function getProperties($filter = null) - { - $properties = $this->properties; - - foreach ($this->getTraitProperties(null) as $traitProperty) { - if (!isset($properties[$traitProperty->getName()])) { - $properties[$traitProperty->getName()] = $traitProperty->alias($this); - } - } - - if (null !== $this->parentClassName) { - foreach ($this->getParentClass()->getProperties(null) as $parentProperty) { - if (!isset($properties[$parentProperty->getName()])) { - $properties[$parentProperty->getName()] = $parentProperty; - } - } - } - - if (null !== $filter) { - $properties = array_filter($properties, function(IReflectionProperty $property) use ($filter) { - return (bool) ($property->getModifiers() & $filter); - }); - } - - return array_values($properties); - } - - /** - * Returns if the class (and not its parents) defines the given property. - * - * @param string $name Property name - * @return boolean - */ - public function hasOwnProperty($name) - { - return isset($this->properties[$name]); - } - - /** - * Returns reflections of properties declared by this class, not its parents. - * - * @param integer $filter Properties filter - * @return array - */ - public function getOwnProperties($filter = null) - { - $properties = $this->properties; - - if (null !== $filter) { - $properties = array_filter($properties, function(ReflectionProperty $property) use ($filter) { - return (bool) ($property->getModifiers() & $filter); - }); - } - - return array_values($properties); - } - - /** - * Returns if the class imports the given property from traits. - * - * @param string $name Property name - * @return boolean - */ - public function hasTraitProperty($name) - { - if (isset($this->properties[$name])) { - return false; - } - - foreach ($this->getOwnTraits() as $trait) { - if ($trait->hasProperty($name)) { - return true; - } - } - - return false; - } - - /** - * Returns reflections of properties imported from traits. - * - * @param integer $filter Properties filter - * @return array - */ - public function getTraitProperties($filter = null) - { - $properties = array(); - - foreach ($this->getOwnTraits() as $trait) { - foreach ($trait->getProperties(null) as $traitProperty) { - if (!isset($this->properties[$traitProperty->getName()]) && !isset($properties[$traitProperty->getName()])) { - $properties[$traitProperty->getName()] = $traitProperty->alias($this); - } - } - } - - if (null !== $filter) { - $properties = array_filter($properties, function(IReflectionProperty $property) use ($filter) { - return (bool) ($property->getModifiers() & $filter); - }); - } - - return array_values($properties); - } - - /** - * Returns default properties. - * - * @return array - */ - public function getDefaultProperties() - { - static $accessLevels = array(InternalReflectionProperty::IS_PUBLIC, InternalReflectionProperty::IS_PROTECTED, InternalReflectionProperty::IS_PRIVATE); - - $defaults = array(); - $properties = $this->getProperties(); - foreach (array(true, false) as $static) { - foreach ($properties as $property) { - foreach ($accessLevels as $level) { - if ($property->isStatic() === $static && ($property->getModifiers() & $level)) { - $defaults[$property->getName()] = $property->getDefaultValue(); - } - } - } - } - - return $defaults; - } - - /** - * Returns static properties reflections. - * - * @return array - */ - public function getStaticProperties() - { - $defaults = array(); - foreach ($this->getProperties(InternalReflectionProperty::IS_STATIC) as $property) { - if ($property instanceof ReflectionProperty) { - $defaults[$property->getName()] = $property->getDefaultValue(); - } - } - - return $defaults; - } - - /** - * Returns a value of a static property. - * - * @param string $name Property name - * @param mixed $default Default value - * @return mixed - * @throws \TokenReflection\Exception\RuntimeException If the requested static property does not exist. - * @throws \TokenReflection\Exception\RuntimeException If the requested static property is not accessible. - */ - public function getStaticPropertyValue($name, $default = null) - { - if ($this->hasProperty($name) && ($property = $this->getProperty($name)) && $property->isStatic()) { - if (!$property->isPublic() && !$property->isAccessible()) { - throw new Exception\RuntimeException(sprintf('Static property "%s" is not accessible.', $name), Exception\RuntimeException::NOT_ACCESSBILE, $this); - } - - return $property->getDefaultValue(); - } - - throw new Exception\RuntimeException(sprintf('There is no static property "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns traits used by this class. - * - * @return array - */ - public function getTraits() - { - $traitNames = $this->getTraitNames(); - if (empty($traitNames)) { - return array(); - } - - $broker = $this->getBroker(); - return array_combine($traitNames, array_map(function($traitName) use ($broker) { - return $broker->getClass($traitName); - }, $traitNames)); - } - - /** - * Returns traits used by this class and not its parents. - * - * @return array - */ - public function getOwnTraits() - { - $ownTraitNames = $this->getOwnTraitNames(); - if (empty($ownTraitNames)) { - return array(); - } - - $broker = $this->getBroker(); - return array_combine($ownTraitNames, array_map(function($traitName) use ($broker) { - return $broker->getClass($traitName); - }, $ownTraitNames)); - } - - /** - * Returns names of used traits. - * - * @return array - */ - public function getTraitNames() - { - $parentClass = $this->getParentClass(); - - $names = $parentClass ? $parentClass->getTraitNames() : array(); - foreach ($this->traits as $traitName) { - $names[] = $traitName; - } - - return array_unique($names); - } - - /** - * Returns names of traits used by this class an not its parents. - * - * @return array - */ - public function getOwnTraitNames() - { - return $this->traits; - } - - /** - * Returns method aliases from traits. - * - * @return array - */ - public function getTraitAliases() - { - return $this->traitAliases; - } - - /** - * Returns if the class is a trait. - * - * @return boolean - */ - public function isTrait() - { - return self::IS_TRAIT === $this->type; - } - - /** - * Returns if the class definition is valid. - * - * @return boolean - */ - public function isValid() - { - if (null !== $this->parentClassName && !$this->getParentClass()->isValid()) { - return false; - } - - foreach ($this->getInterfaces() as $interface) { - if (!$interface->isValid()) { - return false; - } - } - - foreach ($this->getTraits() as $trait) { - if (!$trait->isValid()) { - return false; - } - } - - return true; - } - - /** - * Returns if the class uses a particular trait. - * - * @param \ReflectionClass|\TokenReflection\IReflectionClass|string $trait Trait reflection or name - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If an invalid parameter was provided. - */ - public function usesTrait($trait) - { - if (is_object($trait)) { - if (!$trait instanceof InternalReflectionClass && !$trait instanceof IReflectionClass) { - throw new Exception\RuntimeException(sprintf('Parameter must be a string or an instance of trait reflection, "%s" provided.', get_class($trait)), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $traitName = $trait->getName(); - - if (!$trait->isTrait()) { - throw new Exception\RuntimeException(sprintf('"%s" is not a trait.', $traitName), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - } else { - $reflection = $this->getBroker()->getClass($trait); - if (!$reflection->isTrait()) { - throw new Exception\RuntimeException(sprintf('"%s" is not a trait.', $trait), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $traitName = $trait; - } - - return in_array($traitName, $this->getTraitNames()); - } - - /** - * Returns reflections of direct subclasses. - * - * @return array - */ - public function getDirectSubclasses() - { - $that = $this->name; - return array_filter($this->getBroker()->getClasses(), function(ReflectionClass $class) use ($that) { - if (!$class->isSubclassOf($that)) { - return false; - } - - return null === $class->getParentClassName() || !$class->getParentClass()->isSubClassOf($that); - }); - } - - /** - * Returns names of direct subclasses. - * - * @return array - */ - public function getDirectSubclassNames() - { - return array_keys($this->getDirectSubclasses()); - } - - /** - * Returns reflections of indirect subclasses. - * - * @return array - */ - public function getIndirectSubclasses() - { - $that = $this->name; - return array_filter($this->getBroker()->getClasses(), function(ReflectionClass $class) use ($that) { - if (!$class->isSubclassOf($that)) { - return false; - } - - return null !== $class->getParentClassName() && $class->getParentClass()->isSubClassOf($that); - }); - } - - /** - * Returns names of indirect subclasses. - * - * @return array - */ - public function getIndirectSubclassNames() - { - return array_keys($this->getIndirectSubclasses()); - } - - /** - * Returns reflections of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementers() - { - if (!$this->isInterface()) { - return array(); - } - - $that = $this->name; - return array_filter($this->getBroker()->getClasses(), function(ReflectionClass $class) use ($that) { - if ($class->isInterface() || !$class->implementsInterface($that)) { - return false; - } - - return null === $class->getParentClassName() || !$class->getParentClass()->implementsInterface($that); - }); - } - - /** - * Returns names of classes directly implementing this interface. - * - * @return array - */ - public function getDirectImplementerNames() - { - return array_keys($this->getDirectImplementers()); - } - - /** - * Returns reflections of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementers() - { - if (!$this->isInterface()) { - return array(); - } - - $that = $this->name; - return array_filter($this->getBroker()->getClasses(), function(ReflectionClass $class) use ($that) { - if ($class->isInterface() || !$class->implementsInterface($that)) { - return false; - } - - return null !== $class->getParentClassName() && $class->getParentClass()->implementsInterface($that); - }); - } - - /** - * Returns names of classes indirectly implementing this interface. - * - * @return array - */ - public function getIndirectImplementerNames() - { - return array_keys($this->getIndirectImplementers()); - } - - /** - * Returns if the given object is an instance of this class. - * - * @param object $object Instance - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If the provided argument is not an object. - */ - public function isInstance($object) - { - if (!is_object($object)) { - throw new Exception\RuntimeException(sprintf('Parameter must be an object, "%s" provided.', gettype($object)), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - return $this->name === get_class($object) || is_subclass_of($object, $this->getName()); - } - - /** - * Creates a new class instance without using a constructor. - * - * @return object - * @throws \TokenReflection\Exception\RuntimeException If the class inherits from an internal class. - */ - public function newInstanceWithoutConstructor() - { - if (!class_exists($this->name, true)) { - throw new Exception\RuntimeException('Could not create an instance; class does not exist.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $reflection = new \TokenReflection\Php\ReflectionClass($this->getName(), $this->getBroker()); - return $reflection->newInstanceWithoutConstructor(); - } - - /** - * Creates a new instance using variable number of parameters. - * - * Use any number of constructor parameters as function parameters. - * - * @param mixed $args - * @return object - */ - public function newInstance($args) - { - return $this->newInstanceArgs(func_get_args()); - } - - /** - * Creates a new instance using an array of parameters. - * - * @param array $args Array of constructor parameters - * @return object - * @throws \TokenReflection\Exception\RuntimeException If the required class does not exist. - */ - public function newInstanceArgs(array $args = array()) - { - if (!class_exists($this->name, true)) { - throw new Exception\RuntimeException('Could not create an instance; class does not exist.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $reflection = new InternalReflectionClass($this->name); - return $reflection->newInstanceArgs($args); - } - - /** - * Sets a static property value. - * - * @param string $name Property name - * @param mixed $value Property value - * @throws \TokenReflection\Exception\RuntimeException If the requested static property does not exist. - * @throws \TokenReflection\Exception\RuntimeException If the requested static property is not accessible. - */ - public function setStaticPropertyValue($name, $value) - { - if ($this->hasProperty($name) && ($property = $this->getProperty($name)) && $property->isStatic()) { - if (!$property->isPublic() && !$property->isAccessible()) { - throw new Exception\RuntimeException(sprintf('Static property "%s" is not accessible.', $name), Exception\RuntimeException::NOT_ACCESSBILE, $this); - } - - $property->setDefaultValue($value); - return; - } - - throw new Exception\RuntimeException(sprintf('There is no static property "%s".', $name), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - $implements = ''; - $interfaceNames = $this->getInterfaceNames(); - if (count($interfaceNames) > 0) { - $implements = sprintf( - ' %s %s', - $this->isInterface() ? 'extends' : 'implements', - implode(', ', $interfaceNames) - ); - } - - $buffer = ''; - $count = 0; - foreach ($this->getConstantReflections() as $constant) { - $buffer .= ' ' . $constant->__toString(); - $count++; - } - $constants = sprintf("\n\n - Constants [%d] {\n%s }", $count, $buffer); - - $sBuffer = ''; - $sCount = 0; - $buffer = ''; - $count = 0; - foreach ($this->getProperties() as $property) { - $string = ' ' . preg_replace('~\n(?!$)~', "\n ", $property->__toString()); - if ($property->isStatic()) { - $sBuffer .= $string; - $sCount++; - } else { - $buffer .= $string; - $count++; - } - } - $staticProperties = sprintf("\n\n - Static properties [%d] {\n%s }", $sCount, $sBuffer); - $properties = sprintf("\n\n - Properties [%d] {\n%s }", $count, $buffer); - - $sBuffer = ''; - $sCount = 0; - $buffer = ''; - $count = 0; - foreach ($this->getMethods() as $method) { - // Skip private methods of parent classes - if ($method->getDeclaringClassName() !== $this->getName() && $method->isPrivate()) { - continue; - } - // Indent - $string = "\n "; - - $string .= preg_replace('~\n(?!$|\n|\s*\*)~', "\n ", $method->__toString()); - // Add inherits - if ($method->getDeclaringClassName() !== $this->getName()) { - $string = preg_replace( - array('~Method [ <[\w:]+~', '~, overwrites[^,]+~'), - array('\0, inherits ' . $method->getDeclaringClassName(), ''), - $string - ); - } - if ($method->isStatic()) { - $sBuffer .= $string; - $sCount++; - } else { - $buffer .= $string; - $count++; - } - } - $staticMethods = sprintf("\n\n - Static methods [%d] {\n%s }", $sCount, ltrim($sBuffer, "\n")); - $methods = sprintf("\n\n - Methods [%d] {\n%s }", $count, ltrim($buffer, "\n")); - - return sprintf( - "%s%s [ %s %s%s%s %s%s%s ] {\n @@ %s %d-%d%s%s%s%s%s\n}\n", - $this->getDocComment() ? $this->getDocComment() . "\n" : '', - $this->isInterface() ? 'Interface' : 'Class', - $this->isIterateable() ? ' ' : '', - $this->isAbstract() && !$this->isInterface() ? 'abstract ' : '', - $this->isFinal() ? 'final ' : '', - $this->isInterface() ? 'interface' : 'class', - $this->getName(), - null !== $this->getParentClassName() ? ' extends ' . $this->getParentClassName() : '', - $implements, - $this->getFileName(), - $this->getStartLine(), - $this->getEndLine(), - $constants, - $staticProperties, - $staticMethods, - $properties, - $methods - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string|object $className Class name or class instance - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $className, $return = false) - { - if (is_object($className)) { - $className = get_class($className); - } - - $class = $broker->getClass($className); - if ($class instanceof Invalid\ReflectionClass) { - throw new Exception\RuntimeException('Class is invalid.', Exception\RuntimeException::UNSUPPORTED); - } elseif ($class instanceof Dummy\ReflectionClass) { - throw new Exception\RuntimeException('Class does not exist.', Exception\RuntimeException::DOES_NOT_EXIST); - } - - if ($return) { - return $class->__toString(); - } - - echo $class->__toString(); - } - - /** - * Returns if the class definition is complete. - * - * @return boolean - */ - public function isComplete() - { - if (!$this->definitionComplete) { - if (null !== $this->parentClassName && !$this->getParentClass()->isComplete()) { - return false; - } - - foreach ($this->getOwnInterfaces() as $interface) { - if (!$interface->isComplete()) { - return false; - } - } - - $this->definitionComplete = true; - } - - return $this->definitionComplete; - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->aliases; - } - - /** - * Processes the parent reflection object. - * - * @param \TokenReflection\IReflection $parent Parent reflection object - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionClass - * @throws \TokenReflection\ParseException On invalid parent reflection provided - */ - protected function processParent(IReflection $parent, Stream $tokenStream) - { - if (!$parent instanceof ReflectionFileNamespace) { - throw new Exception\ParseException($this, $tokenStream, sprintf('Invalid parent reflection provided: "%s".', get_class($parent)), Exception\ParseException::INVALID_PARENT); - } - - $this->namespaceName = $parent->getName(); - $this->aliases = $parent->getNamespaceAliases(); - return parent::processParent($parent, $tokenStream); - } - - /** - * Parses reflected element metadata from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionClass - */ - protected function parse(Stream $tokenStream, IReflection $parent) - { - return $this - ->parseModifiers($tokenStream) - ->parseName($tokenStream) - ->parseParent($tokenStream, $parent) - ->parseInterfaces($tokenStream, $parent); - } - - /** - * Parses class modifiers (abstract, final) and class type (class, interface). - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionClass - */ - private function parseModifiers(Stream $tokenStream) - { - while (true) { - switch ($tokenStream->getType()) { - case null: - break 2; - case T_ABSTRACT: - $this->modifiers = InternalReflectionClass::IS_EXPLICIT_ABSTRACT; - break; - case T_FINAL: - $this->modifiers = InternalReflectionClass::IS_FINAL; - break; - case T_INTERFACE: - $this->modifiers = self::IS_INTERFACE; - $this->type = self::IS_INTERFACE; - $tokenStream->skipWhitespaces(true); - break 2; - case T_TRAIT: - $this->modifiers = self::IS_TRAIT; - $this->type = self::IS_TRAIT; - $tokenStream->skipWhitespaces(true); - break 2; - case T_CLASS: - $tokenStream->skipWhitespaces(true); - break 2; - default: - break; - } - - $tokenStream->skipWhitespaces(true); - } - - return $this; - } - - /** - * Parses the class/interface name. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionClass - * @throws \TokenReflection\Exception\ParseException If the class name could not be determined. - */ - protected function parseName(Stream $tokenStream) - { - if (!$tokenStream->is(T_STRING)) { - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - if ($this->namespaceName === ReflectionNamespace::NO_NAMESPACE_NAME) { - $this->name = $tokenStream->getTokenValue(); - } else { - $this->name = $this->namespaceName . '\\' . $tokenStream->getTokenValue(); - } - - $tokenStream->skipWhitespaces(true); - - return $this; - } - - /** - * Parses the parent class. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionClass - */ - private function parseParent(Stream $tokenStream, ReflectionElement $parent = null) - { - if (!$tokenStream->is(T_EXTENDS)) { - return $this; - } - - while (true) { - $tokenStream->skipWhitespaces(true); - - $parentClassName = ''; - while (true) { - switch ($tokenStream->getType()) { - case T_STRING: - case T_NS_SEPARATOR: - $parentClassName .= $tokenStream->getTokenValue(); - break; - default: - break 2; - } - - $tokenStream->skipWhitespaces(true); - } - - $parentClassName = Resolver::resolveClassFQN($parentClassName, $this->aliases, $this->namespaceName); - - if ($this->isInterface()) { - $this->interfaces[] = $parentClassName; - - if (',' === $tokenStream->getTokenValue()) { - continue; - } - } else { - $this->parentClassName = $parentClassName; - } - - break; - } - - return $this; - } - - /** - * Parses implemented interfaces. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionClass - * @throws \TokenReflection\Exception\ParseException On error while parsing interfaces. - */ - private function parseInterfaces(Stream $tokenStream, ReflectionElement $parent = null) - { - if (!$tokenStream->is(T_IMPLEMENTS)) { - return $this; - } - - if ($this->isInterface()) { - throw new Exception\ParseException($this, $tokenStream, 'Interfaces cannot implement interfaces.', Exception\ParseException::LOGICAL_ERROR); - } - - while (true) { - $interfaceName = ''; - - $tokenStream->skipWhitespaces(true); - while (true) { - switch ($tokenStream->getType()) { - case T_STRING: - case T_NS_SEPARATOR: - $interfaceName .= $tokenStream->getTokenValue(); - break; - default: - break 2; - } - - $tokenStream->skipWhitespaces(true); - } - - $this->interfaces[] = Resolver::resolveClassFQN($interfaceName, $this->aliases, $this->namespaceName); - - $type = $tokenStream->getType(); - if ('{' === $type) { - break; - } elseif (',' !== $type) { - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found, expected "{" or ";".', Exception\ParseException::UNEXPECTED_TOKEN); - } - } - - return $this; - } - - /** - * Parses child reflection objects from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionClass - * @throws \TokenReflection\Exception\ParseException If a parse error was detected. - */ - protected function parseChildren(Stream $tokenStream, IReflection $parent) - { - while (true) { - switch ($type = $tokenStream->getType()) { - case null: - break 2; - case T_COMMENT: - case T_DOC_COMMENT: - $docblock = $tokenStream->getTokenValue(); - if (preg_match('~^' . preg_quote(self::DOCBLOCK_TEMPLATE_START, '~') . '~', $docblock)) { - array_unshift($this->docblockTemplates, new ReflectionAnnotation($this, $docblock)); - } elseif (self::DOCBLOCK_TEMPLATE_END === $docblock) { - array_shift($this->docblockTemplates); - } - $tokenStream->next(); - break; - case '}': - break 2; - case T_PUBLIC: - case T_PRIVATE: - case T_PROTECTED: - case T_STATIC: - case T_VAR: - case T_VARIABLE: - static $searching = array(T_VARIABLE => true, T_FUNCTION => true); - - if (T_VAR !== $tokenStream->getType()) { - $position = $tokenStream->key(); - while (null !== ($type = $tokenStream->getType($position)) && !isset($searching[$type])) { - $position++; - } - } - - if (T_VARIABLE === $type || T_VAR === $type) { - $property = new ReflectionProperty($tokenStream, $this->getBroker(), $this); - $this->properties[$property->getName()] = $property; - $tokenStream->next(); - break; - } - // Break missing on purpose - case T_FINAL: - case T_ABSTRACT: - case T_FUNCTION: - $method = new ReflectionMethod($tokenStream, $this->getBroker(), $this); - $this->methods[$method->getName()] = $method; - $tokenStream->next(); - break; - case T_CONST: - $tokenStream->skipWhitespaces(true); - while ($tokenStream->is(T_STRING)) { - $constant = new ReflectionConstant($tokenStream, $this->getBroker(), $this); - $this->constants[$constant->getName()] = $constant; - if ($tokenStream->is(',')) { - $tokenStream->skipWhitespaces(true); - } else { - $tokenStream->next(); - } - } - break; - case T_USE: - $tokenStream->skipWhitespaces(true); - - while (true) { - $traitName = ''; - $type = $tokenStream->getType(); - while (T_STRING === $type || T_NS_SEPARATOR === $type) { - $traitName .= $tokenStream->getTokenValue(); - $type = $tokenStream->skipWhitespaces(true)->getType(); - } - - if ('' === trim($traitName, '\\')) { - throw new Exception\ParseException($this, $tokenStream, 'An empty trait name found.', Exception\ParseException::LOGICAL_ERROR); - } - - $this->traits[] = Resolver::resolveClassFQN($traitName, $this->aliases, $this->namespaceName); - - if (';' === $type) { - // End of "use" - $tokenStream->skipWhitespaces(); - break; - } elseif (',' === $type) { - // Next trait name follows - $tokenStream->skipWhitespaces(); - continue; - } elseif ('{' !== $type) { - // Unexpected token - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found: "%s".', Exception\ParseException::UNEXPECTED_TOKEN); - } - - // Aliases definition - $type = $tokenStream->skipWhitespaces(true)->getType(); - while (true) { - if ('}' === $type) { - $tokenStream->skipWhitespaces(); - break 2; - } - - $leftSide = ''; - $rightSide = array('', null); - $alias = true; - - while (T_STRING === $type || T_NS_SEPARATOR === $type || T_DOUBLE_COLON === $type) { - $leftSide .= $tokenStream->getTokenValue(); - $type = $tokenStream->skipWhitespaces(true)->getType(); - } - - if (T_INSTEADOF === $type) { - $alias = false; - } elseif (T_AS !== $type) { - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - $type = $tokenStream->skipWhitespaces(true)->getType(); - - if (T_PUBLIC === $type || T_PROTECTED === $type || T_PRIVATE === $type) { - if (!$alias) { - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - switch ($type) { - case T_PUBLIC: - $type = InternalReflectionMethod::IS_PUBLIC; - break; - case T_PROTECTED: - $type = InternalReflectionMethod::IS_PROTECTED; - break; - case T_PRIVATE: - $type = InternalReflectionMethod::IS_PRIVATE; - break; - default: - break; - } - - $rightSide[1] = $type; - $type = $tokenStream->skipWhitespaces(true)->getType(); - } - - while (T_STRING === $type || (T_NS_SEPARATOR === $type && !$alias)) { - $rightSide[0] .= $tokenStream->getTokenValue(); - $type = $tokenStream->skipWhitespaces(true)->getType(); - } - - if (empty($leftSide)) { - throw new Exception\ParseException($this, $tokenStream, 'An empty method name was found.', Exception\ParseException::LOGICAL_ERROR); - } - - if ($alias) { - // Alias - if ($pos = strpos($leftSide, '::')) { - $methodName = substr($leftSide, $pos + 2); - $className = Resolver::resolveClassFQN(substr($leftSide, 0, $pos), $this->aliases, $this->namespaceName); - $leftSide = $className . '::' . $methodName; - - $this->traitAliases[$rightSide[0]] = $leftSide; - } else { - $this->traitAliases[$rightSide[0]] = '(null)::' . $leftSide; - } - - $this->traitImports[$leftSide][] = $rightSide; - } else { - // Insteadof - if ($pos = strpos($leftSide, '::')) { - $methodName = substr($leftSide, $pos + 2); - } else { - throw new Exception\ParseException($this, $tokenStream, 'A T_DOUBLE_COLON has to be present when using T_INSTEADOF.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - $this->traitImports[Resolver::resolveClassFQN($rightSide[0], $this->aliases, $this->namespaceName) . '::' . $methodName][] = null; - } - - if (',' === $type) { - $tokenStream->skipWhitespaces(true); - continue; - } elseif (';' !== $type) { - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - $type = $tokenStream->skipWhitespaces()->getType(); - } - } - - break; - default: - $tokenStream->next(); - break; - } - } - - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionConstant.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionConstant.php deleted file mode 100644 index dbd06ad24e9..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionConstant.php +++ /dev/null @@ -1,392 +0,0 @@ -getName(); - if (null !== $this->namespaceName && $this->namespaceName !== ReflectionNamespace::NO_NAMESPACE_NAME) { - $name = substr($name, strlen($this->namespaceName) + 1); - } - - return $name; - } - - /** - * Returns the name of the declaring class. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->declaringClassName; - } - - /** - * Returns a reflection of the declaring class. - * - * @return \TokenReflection\ReflectionClass|null - */ - public function getDeclaringClass() - { - if (null === $this->declaringClassName) { - return null; - } - - return $this->getBroker()->getClass($this->declaringClassName); - } - - /** - * Returns the namespace name. - * - * @return string - */ - public function getNamespaceName() - { - return null === $this->namespaceName || $this->namespaceName === ReflectionNamespace::NO_NAMESPACE_NAME ? '' : $this->namespaceName; - } - - /** - * Returns if the class is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return '' !== $this->getNamespaceName(); - } - - /** - * Returns the constant value. - * - * @return mixed - */ - public function getValue() - { - if (is_array($this->valueDefinition)) { - $this->value = Resolver::getValueDefinition($this->valueDefinition, $this); - $this->valueDefinition = Resolver::getSourceCode($this->valueDefinition); - } - - return $this->value; - } - - /** - * Returns the constant value definition. - * - * @return string - */ - public function getValueDefinition() - { - return is_array($this->valueDefinition) ? Resolver::getSourceCode($this->valueDefinition) : $this->valueDefinition; - } - - /** - * Returns the originaly provided value definition. - * - * @return string - */ - public function getOriginalValueDefinition() - { - return $this->valueDefinition; - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - return sprintf( - "Constant [ %s %s ] { %s }\n", - strtolower(gettype($this->getValue())), - $this->getName(), - $this->getValue() - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string|object|null $class Class name, class instance or null - * @param string $constant Constant name - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $class, $constant, $return = false) - { - $className = is_object($class) ? get_class($class) : $class; - $constantName = $constant; - - if (null === $className) { - $constant = $broker->getConstant($constantName); - if (null === $constant) { - throw new Exception\RuntimeException('Constant does not exist.', Exception\RuntimeException::DOES_NOT_EXIST); - } - } else { - $class = $broker->getClass($className); - if ($class instanceof Invalid\ReflectionClass) { - throw new Exception\RuntimeException('Class is invalid.', Exception\RuntimeException::UNSUPPORTED); - } elseif ($class instanceof Dummy\ReflectionClass) { - throw new Exception\RuntimeException('Class does not exist.', Exception\RuntimeException::DOES_NOT_EXIST, $class); - } - $constant = $class->getConstantReflection($constantName); - } - - if ($return) { - return $constant->__toString(); - } - - echo $constant->__toString(); - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return null === $this->declaringClassName ? $this->aliases : $this->getDeclaringClass()->getNamespaceAliases(); - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return null === $this->declaringClassName ? parent::getPrettyName() : sprintf('%s::%s', $this->declaringClassName, $this->name); - } - - /** - * Returns if the constant definition is valid. - * - * @return boolean - */ - public function isValid() - { - return true; - } - - /** - * Processes the parent reflection object. - * - * @param \TokenReflection\IReflection $parent Parent reflection object - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionElement - * @throws \TokenReflection\Exception\ParseException If an invalid parent reflection object was provided. - */ - protected function processParent(IReflection $parent, Stream $tokenStream) - { - if ($parent instanceof ReflectionFileNamespace) { - $this->namespaceName = $parent->getName(); - $this->aliases = $parent->getNamespaceAliases(); - } elseif ($parent instanceof ReflectionClass) { - $this->declaringClassName = $parent->getName(); - } else { - throw new Exception\ParseException($this, $tokenStream, sprintf('Invalid parent reflection provided: "%s".', get_class($parent)), Exception\ParseException::INVALID_PARENT); - } - - return parent::processParent($parent, $tokenStream); - } - - /** - * Find the appropriate docblock. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection - * @return \TokenReflection\ReflectionConstant - */ - protected function parseDocComment(Stream $tokenStream, IReflection $parent) - { - $position = $tokenStream->key() - 1; - while ($position > 0 && !$tokenStream->is(T_CONST, $position)) { - $position--; - } - - $actual = $tokenStream->key(); - - parent::parseDocComment($tokenStream->seek($position), $parent); - - $tokenStream->seek($actual); - - return $this; - } - - /** - * Parses reflected element metadata from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionConstant - */ - protected function parse(Stream $tokenStream, IReflection $parent) - { - if ($tokenStream->is(T_CONST)) { - $tokenStream->skipWhitespaces(true); - } - - if (false === $this->docComment->getDocComment()) { - parent::parseDocComment($tokenStream, $parent); - } - - return $this - ->parseName($tokenStream) - ->parseValue($tokenStream, $parent); - } - - /** - * Parses the constant name. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionConstant - * @throws \TokenReflection\Exception\ParseReflection If the constant name could not be determined. - */ - protected function parseName(Stream $tokenStream) - { - if (!$tokenStream->is(T_STRING)) { - throw new Exception\ParseException($this, $tokenStream, 'The constant name could not be determined.', Exception\ParseException::LOGICAL_ERROR); - } - - if (null === $this->namespaceName || $this->namespaceName === ReflectionNamespace::NO_NAMESPACE_NAME) { - $this->name = $tokenStream->getTokenValue(); - } else { - $this->name = $this->namespaceName . '\\' . $tokenStream->getTokenValue(); - } - - $tokenStream->skipWhitespaces(true); - - return $this; - } - - /** - * Parses the constant value. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionConstant - * @throws \TokenReflection\Exception\ParseException If the constant value could not be determined. - */ - private function parseValue(Stream $tokenStream, IReflection $parent) - { - if (!$tokenStream->is('=')) { - throw new Exception\ParseException($this, $tokenStream, 'Could not find the definition start.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - $tokenStream->skipWhitespaces(true); - - static $acceptedTokens = array( - '-' => true, - '+' => true, - T_STRING => true, - T_NS_SEPARATOR => true, - T_CONSTANT_ENCAPSED_STRING => true, - T_DNUMBER => true, - T_LNUMBER => true, - T_DOUBLE_COLON => true, - T_CLASS_C => true, - T_DIR => true, - T_FILE => true, - T_FUNC_C => true, - T_LINE => true, - T_METHOD_C => true, - T_NS_C => true, - T_TRAIT_C => true - ); - - while (null !== ($type = $tokenStream->getType())) { - if (T_START_HEREDOC === $type) { - $this->valueDefinition[] = $tokenStream->current(); - while (null !== $type && T_END_HEREDOC !== $type) { - $tokenStream->next(); - $this->valueDefinition[] = $tokenStream->current(); - $type = $tokenStream->getType(); - }; - $tokenStream->next(); - } elseif (isset($acceptedTokens[$type])) { - $this->valueDefinition[] = $tokenStream->current(); - $tokenStream->next(); - } elseif ($tokenStream->isWhitespace(true)) { - $tokenStream->skipWhitespaces(true); - } else { - break; - } - } - - if (empty($this->valueDefinition)) { - throw new Exception\ParseException($this, $tokenStream, 'Value definition is empty.', Exception\ParseException::LOGICAL_ERROR); - } - - $value = $tokenStream->getTokenValue(); - if (null === $type || (',' !== $value && ';' !== $value)) { - throw new Exception\ParseException($this, $tokenStream, 'Invalid value definition.', Exception\ParseException::LOGICAL_ERROR); - } - - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionElement.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionElement.php deleted file mode 100644 index b0fb331eb23..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionElement.php +++ /dev/null @@ -1,352 +0,0 @@ -count()) { - throw new Exception\ParseException($this, $tokenStream, 'Reflection token stream must not be empty.', Exception\ParseException::INVALID_ARGUMENT); - } - - parent::__construct($tokenStream, $broker, $parent); - } - - /** - * Parses the token substream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - */ - final protected function parseStream(Stream $tokenStream, IReflection $parent = null) - { - $this->fileName = $tokenStream->getFileName(); - - $this - ->processParent($parent, $tokenStream) - ->parseStartLine($tokenStream) - ->parseDocComment($tokenStream, $parent) - ->parse($tokenStream, $parent) - ->parseChildren($tokenStream, $parent) - ->parseEndLine($tokenStream); - } - - /** - * Returns the file name the reflection object is defined in. - * - * @return string - */ - public function getFileName() - { - return $this->fileName; - } - - /** - * Returns a file reflection. - * - * @return \TokenReflection\ReflectionFile - * @throws \TokenReflection\Exception\RuntimeException If the file is not stored inside the broker - */ - public function getFileReflection() - { - return $this->getBroker()->getFile($this->fileName); - } - - /** - * Returns the definition start line number in the file. - * - * @return integer - */ - public function getStartLine() - { - return $this->startLine; - } - - /** - * Returns the definition end line number in the file. - * - * @return integer - */ - public function getEndLine() - { - return $this->endLine; - } - - /** - * Returns the PHP extension reflection. - * - * Alwyas returns null - everything is user defined. - * - * @return null - */ - public function getExtension() - { - return null; - } - - /** - * Returns the PHP extension name. - * - * Alwyas returns false - everything is user defined. - * - * @return boolean - */ - public function getExtensionName() - { - return false; - } - - /** - * Returns the appropriate source code part. - * - * @return string - */ - public function getSource() - { - return $this->broker->getFileTokens($this->getFileName())->getSourcePart($this->startPosition, $this->endPosition); - } - - /** - * Returns the start position in the file token stream. - * - * @return integer - */ - public function getStartPosition() - { - return $this->startPosition; - } - - /** - * Returns the end position in the file token stream. - * - * @return integer - */ - public function getEndPosition() - { - return $this->endPosition; - } - - /** - * Returns the stack of docblock templates. - * - * @return array - */ - protected function getDocblockTemplates() - { - return $this->docblockTemplates; - } - - /** - * Processes the parent reflection object. - * - * @param \TokenReflection\Reflection $parent Parent reflection object - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionElement - */ - protected function processParent(IReflection $parent, Stream $tokenStream) - { - // To be defined in child classes - return $this; - } - - /** - * Find the appropriate docblock. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection - * @return \TokenReflection\ReflectionElement - */ - protected function parseDocComment(Stream $tokenStream, IReflection $parent) - { - if ($this instanceof ReflectionParameter) { - $this->docComment = new ReflectionAnnotation($this); - return $this; - } - - $position = $tokenStream->key(); - - if ($tokenStream->is(T_DOC_COMMENT, $position - 1)) { - $value = $tokenStream->getTokenValue($position - 1); - if (self::DOCBLOCK_TEMPLATE_END !== $value) { - $this->docComment = new ReflectionAnnotation($this, $value); - $this->startPosition--; - } - } elseif ($tokenStream->is(T_DOC_COMMENT, $position - 2)) { - $value = $tokenStream->getTokenValue($position - 2); - if (self::DOCBLOCK_TEMPLATE_END !== $value) { - $this->docComment = new ReflectionAnnotation($this, $value); - $this->startPosition -= 2; - } - } elseif ($tokenStream->is(T_COMMENT, $position - 1) && preg_match('~^' . preg_quote(self::DOCBLOCK_TEMPLATE_START, '~') . '~', $tokenStream->getTokenValue($position - 1))) { - $this->docComment = new ReflectionAnnotation($this, $tokenStream->getTokenValue($position - 1)); - $this->startPosition--; - } elseif ($tokenStream->is(T_COMMENT, $position - 2) && preg_match('~^' . preg_quote(self::DOCBLOCK_TEMPLATE_START, '~') . '~', $tokenStream->getTokenValue($position - 2))) { - $this->docComment = new ReflectionAnnotation($this, $tokenStream->getTokenValue($position - 2)); - $this->startPosition -= 2; - } - - if (null === $this->docComment) { - $this->docComment = new ReflectionAnnotation($this); - } - - if ($parent instanceof ReflectionElement) { - $this->docComment->setTemplates($parent->getDocblockTemplates()); - } - - return $this; - } - - /** - * Saves the start line number. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token susbtream - * @return \TokenReflection\ReflectionElement - */ - private final function parseStartLine(Stream $tokenStream) - { - $token = $tokenStream->current(); - $this->startLine = $token[2]; - - $this->startPosition = $tokenStream->key(); - - return $this; - } - - /** - * Saves the end line number. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token susbtream - * @return \TokenReflection\ReflectionElement - */ - private final function parseEndLine(Stream $tokenStream) - { - $token = $tokenStream->current(); - $this->endLine = $token[2]; - - $this->endPosition = $tokenStream->key(); - - return $this; - } - - /** - * Parses reflected element metadata from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionElement - */ - abstract protected function parse(Stream $tokenStream, IReflection $parent); - - /** - * Parses the reflection object name. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionElement - */ - abstract protected function parseName(Stream $tokenStream); - - /** - * Parses child reflection objects from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\Reflection $parent Parent reflection object - * @return \TokenReflection\ReflectionElement - */ - protected function parseChildren(Stream $tokenStream, IReflection $parent) - { - // To be defined in child classes - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionFile.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionFile.php deleted file mode 100644 index 863b3587299..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionFile.php +++ /dev/null @@ -1,144 +0,0 @@ -namespaces; - } - - /** - * Returns the string representation of the reflection object. - * - * @throws \TokenReflection\Exception\RuntimeException If the method is called, because it's unsupported. - */ - public function __toString() - { - throw new Exception\RuntimeException('Casting to string is not supported.', Exception\RuntimeException::UNSUPPORTED, $this); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string $argument Reflection object name - * @param boolean $return Return the export instead of outputting it - * @throws \TokenReflection\Exception\RuntimeException If the method is called, because it's unsupported. - */ - public static function export(Broker $broker, $argument, $return = false) - { - throw new Exception\RuntimeException('Export is not supported.', Exception\RuntimeException::UNSUPPORTED); - } - - /** - * Outputs the file source code. - * - * @return string - */ - public function getSource() - { - return (string) $this->broker->getFileTokens($this->getName()); - } - - /** - * Parses the token substream and prepares namespace reflections from the file. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionFile - */ - protected function parseStream(Stream $tokenStream, IReflection $parent = null) - { - $this->name = $tokenStream->getFileName(); - - if (1 >= $tokenStream->count()) { - // No PHP content - $this->docComment = new ReflectionAnnotation($this, null); - return $this; - } - - $docCommentPosition = null; - - if (!$tokenStream->is(T_OPEN_TAG)) { - $this->namespaces[] = new ReflectionFileNamespace($tokenStream, $this->broker, $this); - } else { - $tokenStream->skipWhitespaces(); - - while (null !== ($type = $tokenStream->getType())) { - switch ($type) { - case T_DOC_COMMENT: - if (null === $docCommentPosition) { - $docCommentPosition = $tokenStream->key(); - } - case T_WHITESPACE: - case T_COMMENT: - break; - case T_DECLARE: - // Intentionally twice call of skipWhitespaces() - $tokenStream - ->skipWhitespaces() - ->findMatchingBracket() - ->skipWhitespaces() - ->skipWhitespaces(); - break; - case T_NAMESPACE: - $docCommentPosition = $docCommentPosition ?: -1; - break 2; - default: - $docCommentPosition = $docCommentPosition ?: -1; - $this->namespaces[] = new ReflectionFileNamespace($tokenStream, $this->broker, $this); - break 2; - } - - $tokenStream->skipWhitespaces(); - } - - while (null !== ($type = $tokenStream->getType())) { - if (T_NAMESPACE === $type) { - $this->namespaces[] = new ReflectionFileNamespace($tokenStream, $this->broker, $this); - } else { - $tokenStream->skipWhitespaces(); - } - } - } - - if (null !== $docCommentPosition && !empty($this->namespaces) && $docCommentPosition === $this->namespaces[0]->getStartPosition()) { - $docCommentPosition = null; - } - $this->docComment = new ReflectionAnnotation($this, null !== $docCommentPosition ? $tokenStream->getTokenValue($docCommentPosition) : null); - - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionFileNamespace.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionFileNamespace.php deleted file mode 100644 index 6304256e457..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionFileNamespace.php +++ /dev/null @@ -1,412 +0,0 @@ -classes; - } - - /** - * Returns constant reflections. - * - * @return array - */ - public function getConstants() - { - return $this->constants; - } - - /** - * Returns function reflections. - * - * @return array - */ - public function getFunctions() - { - return $this->functions; - } - - /** - * Returns all imported namespaces and aliases. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->aliases; - } - - /** - * Processes the parent reflection object. - * - * @param \TokenReflection\IReflection $parent Parent reflection object - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionElement - * @throws \TokenReflection\Exception\ParseException If an invalid parent reflection object was provided. - */ - protected function processParent(IReflection $parent, Stream $tokenStream) - { - if (!$parent instanceof ReflectionFile) { - throw new Exception\ParseException($this, $tokenStream, 'The parent object has to be an instance of TokenReflection\ReflectionFile.', Exception\ParseException::INVALID_PARENT); - } - - return parent::processParent($parent, $tokenStream); - } - - /** - * Parses reflected element metadata from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionFileNamespace - */ - protected function parse(Stream $tokenStream, IReflection $parent) - { - return $this->parseName($tokenStream); - } - - /** - * Find the appropriate docblock. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection - * @return \TokenReflection\ReflectionElement - */ - protected function parseDocComment(Stream $tokenStream, IReflection $parent) - { - if (!$tokenStream->is(T_NAMESPACE)) { - $this->docComment = new ReflectionAnnotation($this); - return $this; - } else { - return parent::parseDocComment($tokenStream, $parent); - } - } - - /** - * Parses the namespace name. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionFileNamespace - * @throws \TokenReflection\Exception\ParseException If the namespace name could not be determined. - */ - protected function parseName(Stream $tokenStream) - { - if (!$tokenStream->is(T_NAMESPACE)) { - $this->name = ReflectionNamespace::NO_NAMESPACE_NAME; - return $this; - } - - $tokenStream->skipWhitespaces(); - - $name = ''; - // Iterate over the token stream - while (true) { - switch ($tokenStream->getType()) { - // If the current token is a T_STRING, it is a part of the namespace name - case T_STRING: - case T_NS_SEPARATOR: - $name .= $tokenStream->getTokenValue(); - break; - default: - // Stop iterating when other token than string or ns separator found - break 2; - } - - $tokenStream->skipWhitespaces(true); - } - - $name = ltrim($name, '\\'); - - if (empty($name)) { - $this->name = ReflectionNamespace::NO_NAMESPACE_NAME; - } else { - $this->name = $name; - } - - if (!$tokenStream->is(';') && !$tokenStream->is('{')) { - throw new Exception\ParseException($this, $tokenStream, 'Invalid namespace name end, expecting ";" or "{".', Exception\ParseException::UNEXPECTED_TOKEN); - } - - $tokenStream->skipWhitespaces(); - - return $this; - } - - /** - * Parses child reflection objects from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionFileNamespace - * @throws \TokenReflection\Exception\ParseException If child elements could not be parsed. - */ - protected function parseChildren(Stream $tokenStream, IReflection $parent) - { - static $skipped = array(T_WHITESPACE => true, T_COMMENT => true, T_DOC_COMMENT => true); - $depth = 0; - - $firstChild = null; - - while (true) { - switch ($tokenStream->getType()) { - case T_USE: - while (true) { - $namespaceName = ''; - $alias = null; - - $tokenStream->skipWhitespaces(true); - - while (true) { - switch ($tokenStream->getType()) { - case T_STRING: - case T_NS_SEPARATOR: - $namespaceName .= $tokenStream->getTokenValue(); - break; - default: - break 2; - } - $tokenStream->skipWhitespaces(true); - } - $namespaceName = ltrim($namespaceName, '\\'); - - if (empty($namespaceName)) { - throw new Exception\ParseException($this, $tokenStream, 'Imported namespace name could not be determined.', Exception\ParseException::LOGICAL_ERROR); - } elseif ('\\' === substr($namespaceName, -1)) { - throw new Exception\ParseException($this, $tokenStream, sprintf('Invalid namespace name "%s".', $namespaceName), Exception\ParseException::LOGICAL_ERROR); - } - - if ($tokenStream->is(T_AS)) { - // Alias defined - $tokenStream->skipWhitespaces(true); - - if (!$tokenStream->is(T_STRING)) { - throw new Exception\ParseException($this, $tokenStream, sprintf('The imported namespace "%s" seems aliased but the alias name could not be determined.', $namespaceName), Exception\ParseException::LOGICAL_ERROR); - } - - $alias = $tokenStream->getTokenValue(); - - $tokenStream->skipWhitespaces(true); - } else { - // No explicit alias - if (false !== ($pos = strrpos($namespaceName, '\\'))) { - $alias = substr($namespaceName, $pos + 1); - } else { - $alias = $namespaceName; - } - } - - if (isset($this->aliases[$alias])) { - throw new Exception\ParseException($this, $tokenStream, sprintf('Namespace alias "%s" already defined.', $alias), Exception\ParseException::LOGICAL_ERROR); - } - - $this->aliases[$alias] = $namespaceName; - - $type = $tokenStream->getType(); - if (';' === $type) { - $tokenStream->skipWhitespaces(); - break 2; - } elseif (',' === $type) { - // Next namespace in the current "use" definition - continue; - } - - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - case T_COMMENT: - case T_DOC_COMMENT: - $docblock = $tokenStream->getTokenValue(); - if (preg_match('~^' . preg_quote(self::DOCBLOCK_TEMPLATE_START, '~') . '~', $docblock)) { - array_unshift($this->docblockTemplates, new ReflectionAnnotation($this, $docblock)); - } elseif (self::DOCBLOCK_TEMPLATE_END === $docblock) { - array_shift($this->docblockTemplates); - } - $tokenStream->next(); - break; - case '{': - $tokenStream->next(); - $depth++; - break; - case '}': - if (0 === $depth--) { - break 2; - } - - $tokenStream->next(); - break; - case null: - case T_NAMESPACE: - break 2; - case T_ABSTRACT: - case T_FINAL: - case T_CLASS: - case T_TRAIT: - case T_INTERFACE: - $class = new ReflectionClass($tokenStream, $this->getBroker(), $this); - $firstChild = $firstChild ?: $class; - - $className = $class->getName(); - if (isset($this->classes[$className])) { - if (!$this->classes[$className] instanceof Invalid\ReflectionClass) { - $this->classes[$className] = new Invalid\ReflectionClass($className, $this->classes[$className]->getFileName(), $this->getBroker()); - } - - if (!$this->classes[$className]->hasReasons()) { - $this->classes[$className]->addReason(new Exception\ParseException( - $this, - $tokenStream, - sprintf('Class %s is defined multiple times in the file.', $className), - Exception\ParseException::ALREADY_EXISTS - )); - } - } else { - $this->classes[$className] = $class; - } - $tokenStream->next(); - break; - case T_CONST: - $tokenStream->skipWhitespaces(true); - do { - $constant = new ReflectionConstant($tokenStream, $this->getBroker(), $this); - $firstChild = $firstChild ?: $constant; - - $constantName = $constant->getName(); - if (isset($this->constants[$constantName])) { - if (!$this->constants[$constantName] instanceof Invalid\ReflectionConstant) { - $this->constants[$constantName] = new Invalid\ReflectionConstant($constantName, $this->constants[$constantName]->getFileName(), $this->getBroker()); - } - - if (!$this->constants[$constantName]->hasReasons()) { - $this->constants[$constantName]->addReason(new Exception\ParseException( - $this, - $tokenStream, - sprintf('Constant %s is defined multiple times in the file.', $constantName), - Exception\ParseException::ALREADY_EXISTS - )); - } - } else { - $this->constants[$constantName] = $constant; - } - if ($tokenStream->is(',')) { - $tokenStream->skipWhitespaces(true); - } else { - $tokenStream->next(); - } - } while ($tokenStream->is(T_STRING)); - break; - case T_FUNCTION: - $position = $tokenStream->key() + 1; - while (isset($skipped[$type = $tokenStream->getType($position)])) { - $position++; - } - if ('(' === $type) { - // Skipping anonymous functions - - $tokenStream - ->seek($position) - ->findMatchingBracket() - ->skipWhiteSpaces(true); - - if ($tokenStream->is(T_USE)) { - $tokenStream - ->skipWhitespaces(true) - ->findMatchingBracket() - ->skipWhitespaces(true); - } - - $tokenStream - ->findMatchingBracket() - ->next(); - - continue; - } - - $function = new ReflectionFunction($tokenStream, $this->getBroker(), $this); - $firstChild = $firstChild ?: $function; - - $functionName = $function->getName(); - if (isset($this->functions[$functionName])) { - if (!$this->functions[$functionName] instanceof Invalid\ReflectionFunction) { - $this->functions[$functionName] = new Invalid\ReflectionFunction($functionName, $this->functions[$functionName]->getFileName(), $this->getBroker()); - } - - if (!$this->functions[$functionName]->hasReasons()) { - $this->functions[$functionName]->addReason(new Exception\ParseException( - $this, - $tokenStream, - sprintf('Function %s is defined multiple times in the file.', $functionName), - Exception\ParseException::ALREADY_EXISTS - )); - } - } else { - $this->functions[$functionName] = $function; - } - $tokenStream->next(); - break; - default: - $tokenStream->next(); - break; - } - } - - if ($firstChild) { - $this->startPosition = min($this->startPosition, $firstChild->getStartPosition()); - } - - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionFunction.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionFunction.php deleted file mode 100644 index 7ef59af1760..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionFunction.php +++ /dev/null @@ -1,204 +0,0 @@ -hasAnnotation('disabled'); - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - $parameters = ''; - if ($this->getNumberOfParameters() > 0) { - $buffer = ''; - foreach ($this->getParameters() as $parameter) { - $buffer .= "\n " . $parameter->__toString(); - } - $parameters = sprintf( - "\n\n - Parameters [%d] {%s\n }", - $this->getNumberOfParameters(), - $buffer - ); - } - return sprintf( - "%sFunction [ function %s%s ] {\n @@ %s %d - %d%s\n}\n", - $this->getDocComment() ? $this->getDocComment() . "\n" : '', - $this->returnsReference() ? '&' : '', - $this->getName(), - $this->getFileName(), - $this->getStartLine(), - $this->getEndLine(), - $parameters - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string $function Function name - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $function, $return = false) - { - $functionName = $function; - - $function = $broker->getFunction($functionName); - if (null === $function) { - throw new Exception\RuntimeException(sprintf('Function %s() does not exist.', $functionName), Exception\RuntimeException::DOES_NOT_EXIST); - } - - if ($return) { - return $function->__toString(); - } - - echo $function->__toString(); - } - - /** - * Calls the function. - * - * @return mixed - */ - public function invoke() - { - return $this->invokeArgs(func_get_args()); - } - - /** - * Calls the function. - * - * @param array $args Function parameter values - * @return mixed - * @throws \TokenReflection\Exception\RuntimeException If the required function does not exist. - */ - public function invokeArgs(array $args = array()) - { - if (!function_exists($this->getName())) { - throw new Exception\RuntimeException('Could not invoke function; function is not defined.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - return call_user_func_array($this->getName(), $args); - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->aliases; - } - - /** - * Returns the function/method as closure. - * - * @return \Closure - */ - public function getClosure() - { - if (!function_exists($this->getName())) { - throw new Exception\RuntimeException('Could not invoke function; function is not defined.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $that = $this; - return function() use ($that) { - return $that->invokeArgs(func_get_args()); - }; - } - - /** - * Returns the closure scope class. - * - * @return null - */ - public function getClosureScopeClass() - { - return null; - } - - /** - * Returns if the function definition is valid. - * - * @return boolean - */ - public function isValid() - { - return true; - } - - /** - * Processes the parent reflection object. - * - * @param \TokenReflection\IReflection $parent Parent reflection object - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionElement - * @throws \TokenReflection\Exception\ParseException If an invalid parent reflection object was provided. - */ - protected function processParent(IReflection $parent, Stream $tokenStream) - { - if (!$parent instanceof ReflectionFileNamespace) { - throw new Exception\ParseException($this, $tokenStream, 'The parent object has to be an instance of TokenReflection\ReflectionFileNamespace.', Exception\ParseException::INVALID_PARENT); - } - - $this->namespaceName = $parent->getName(); - $this->aliases = $parent->getNamespaceAliases(); - return parent::processParent($parent, $tokenStream); - } - - /** - * Parses reflected element metadata from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionFunction - */ - protected function parse(Stream $tokenStream, IReflection $parent) - { - return $this - ->parseReturnsReference($tokenStream) - ->parseName($tokenStream); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionFunctionBase.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionFunctionBase.php deleted file mode 100644 index 786ab1e9ca3..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionFunctionBase.php +++ /dev/null @@ -1,440 +0,0 @@ -namespaceName && ReflectionNamespace::NO_NAMESPACE_NAME !== $this->namespaceName) { - return $this->namespaceName . '\\' . $this->name; - } - - return $this->name; - } - - /** - * Returns the unqualified name (UQN). - * - * @return string - */ - public function getShortName() - { - return $this->name; - } - - /** - * Returns the namespace name. - * - * @return string - */ - public function getNamespaceName() - { - return null === $this->namespaceName || $this->namespaceName === ReflectionNamespace::NO_NAMESPACE_NAME ? '' : $this->namespaceName; - } - - /** - * Returns if the function/method is defined within a namespace. - * - * @return boolean - */ - public function inNamespace() - { - return '' !== $this->getNamespaceName(); - } - - /** - * Returns if the function/method is a closure. - * - * @return boolean - */ - public function isClosure() - { - return false; - } - - /** - * Returns this pointer bound to closure. - * - * @return null - */ - public function getClosureThis() - { - return null; - } - - /** - * Returns the closure scope class. - * - * @return string|null - */ - public function getClosureScopeClass() - { - return null; - } - - /** - * Returns if the function/method returns its value as reference. - * - * @return boolean - */ - public function returnsReference() - { - return $this->returnsReference; - } - - /** - * Returns a particular function/method parameter. - * - * @param integer|string $parameter Parameter name or position - * @return \TokenReflection\ReflectionParameter - * @throws \TokenReflection\Exception\RuntimeException If there is no parameter of the given name. - * @throws \TokenReflection\Exception\RuntimeException If there is no parameter at the given position. - */ - public function getParameter($parameter) - { - if (is_numeric($parameter)) { - if (!isset($this->parameters[$parameter])) { - throw new Exception\RuntimeException(sprintf('There is no parameter at position "%d".', $parameter), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - return $this->parameters[$parameter]; - } else { - foreach ($this->parameters as $reflection) { - if ($reflection->getName() === $parameter) { - return $reflection; - } - } - - throw new Exception\RuntimeException(sprintf('There is no parameter "%s".', $parameter), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - } - - /** - * Returns parameters. - * - * @return array - */ - public function getParameters() - { - return $this->parameters; - } - - /** - * Returns the number of parameters. - * - * @return integer - */ - public function getNumberOfParameters() - { - return count($this->parameters); - } - - /** - * Returns the number of required parameters. - * - * @return integer - */ - public function getNumberOfRequiredParameters() - { - $count = 0; - array_walk($this->parameters, function(ReflectionParameter $parameter) use (&$count) { - if (!$parameter->isOptional()) { - $count++; - } - }); - return $count; - } - - /** - * Returns static variables. - * - * @return array - */ - public function getStaticVariables() - { - if (empty($this->staticVariables) && !empty($this->staticVariablesDefinition)) { - foreach ($this->staticVariablesDefinition as $variableName => $variableDefinition) { - $this->staticVariables[$variableName] = Resolver::getValueDefinition($variableDefinition, $this); - } - } - - return $this->staticVariables; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->name . '()'; - } - - /** - * Creates aliases to parameters. - * - * @throws \TokenReflection\Exception\RuntimeException When called on a ReflectionFunction instance. - */ - protected final function aliasParameters() - { - if (!$this instanceof ReflectionMethod) { - throw new Exception\RuntimeException('Only method parameters can be aliased.', Exception\RuntimeException::UNSUPPORTED, $this); - } - - foreach ($this->parameters as $index => $parameter) { - $this->parameters[$index] = $parameter->alias($this); - } - } - - /** - * Parses if the function/method returns its value as reference. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionFunctionBase - * @throws \TokenReflection\Exception\ParseException If could not be determined if the function\method returns its value by reference. - */ - final protected function parseReturnsReference(Stream $tokenStream) - { - if (!$tokenStream->is(T_FUNCTION)) { - throw new Exception\ParseException($this, $tokenStream, 'Could not find the function keyword.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - $tokenStream->skipWhitespaces(true); - - $type = $tokenStream->getType(); - - if ('&' === $type) { - $this->returnsReference = true; - $tokenStream->skipWhitespaces(true); - } elseif (T_STRING !== $type) { - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - return $this; - } - - /** - * Parses the function/method name. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionMethod - * @throws \TokenReflection\Exception\ParseException If the class name could not be determined. - */ - final protected function parseName(Stream $tokenStream) - { - $this->name = $tokenStream->getTokenValue(); - - $tokenStream->skipWhitespaces(true); - - return $this; - } - - /** - * Parses child reflection objects from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionElement - */ - final protected function parseChildren(Stream $tokenStream, IReflection $parent) - { - return $this - ->parseParameters($tokenStream) - ->parseStaticVariables($tokenStream); - } - - /** - * Parses function/method parameters. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionFunctionBase - * @throws \TokenReflection\Exception\ParseException If parameters could not be parsed. - */ - final protected function parseParameters(Stream $tokenStream) - { - if (!$tokenStream->is('(')) { - throw new Exception\ParseException($this, $tokenStream, 'Could find the start token.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - static $accepted = array(T_NS_SEPARATOR => true, T_STRING => true, T_ARRAY => true, T_CALLABLE => true, T_VARIABLE => true, '&' => true); - - $tokenStream->skipWhitespaces(true); - - while (null !== ($type = $tokenStream->getType()) && ')' !== $type) { - if (isset($accepted[$type])) { - $parameter = new ReflectionParameter($tokenStream, $this->getBroker(), $this); - $this->parameters[] = $parameter; - } - - if ($tokenStream->is(')')) { - break; - } - - $tokenStream->skipWhitespaces(true); - } - - $tokenStream->skipWhitespaces(); - - return $this; - } - - /** - * Parses static variables. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionFunctionBase - * @throws \TokenReflection\Exception\ParseException If static variables could not be parsed. - */ - final protected function parseStaticVariables(Stream $tokenStream) - { - $type = $tokenStream->getType(); - if ('{' === $type) { - if ($this->getBroker()->isOptionSet(Broker::OPTION_PARSE_FUNCTION_BODY)) { - $tokenStream->skipWhitespaces(true); - - while ('}' !== ($type = $tokenStream->getType())) { - switch ($type) { - case T_STATIC: - $type = $tokenStream->skipWhitespaces(true)->getType(); - if (T_VARIABLE !== $type) { - // Late static binding - break; - } - - while (T_VARIABLE === $type) { - $variableName = $tokenStream->getTokenValue(); - $variableDefinition = array(); - - $type = $tokenStream->skipWhitespaces(true)->getType(); - if ('=' === $type) { - $type = $tokenStream->skipWhitespaces(true)->getType(); - $level = 0; - while ($tokenStream->valid()) { - switch ($type) { - case '(': - case '[': - case '{': - case T_CURLY_OPEN: - case T_DOLLAR_OPEN_CURLY_BRACES: - $level++; - break; - case ')': - case ']': - case '}': - $level--; - break; - case ';': - case ',': - if (0 === $level) { - break 2; - } - default: - break; - } - - $variableDefinition[] = $tokenStream->current(); - $type = $tokenStream->skipWhitespaces(true)->getType(); - } - - if (!$tokenStream->valid()) { - throw new Exception\ParseException($this, $tokenStream, 'Invalid end of token stream.', Exception\ParseException::READ_BEYOND_EOS); - } - } - - $this->staticVariablesDefinition[substr($variableName, 1)] = $variableDefinition; - - if (',' === $type) { - $type = $tokenStream->skipWhitespaces(true)->getType(); - } else { - break; - } - } - - break; - case T_FUNCTION: - // Anonymous function -> skip to its end - if (!$tokenStream->find('{')) { - throw new Exception\ParseException($this, $tokenStream, 'Could not find beginning of the anonymous function.', Exception\ParseException::UNEXPECTED_TOKEN); - } - // Break missing intentionally - case '{': - case '[': - case '(': - case T_CURLY_OPEN: - case T_DOLLAR_OPEN_CURLY_BRACES: - $tokenStream->findMatchingBracket()->skipWhitespaces(true); - break; - default: - $tokenStream->skipWhitespaces(); - break; - } - } - } else { - $tokenStream->findMatchingBracket(); - } - } elseif (';' !== $type) { - throw new Exception\ParseException($this, $tokenStream, 'Unexpected token found.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionMethod.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionMethod.php deleted file mode 100644 index eb705426c69..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionMethod.php +++ /dev/null @@ -1,775 +0,0 @@ -declaringClassName ? null : $this->getBroker()->getClass($this->declaringClassName); - } - - /** - * Returns the declaring class name. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->declaringClassName; - } - - /** - * Returns method modifiers. - * - * @return integer - */ - public function getModifiers() - { - if (!$this->modifiersComplete && !($this->modifiers & (self::ACCESS_LEVEL_CHANGED | self::IS_IMPLEMENTED_ABSTRACT))) { - $declaringClass = $this->getDeclaringClass(); - $parentClass = $declaringClass->getParentClass(); - if (false !== $parentClass && $parentClass->hasMethod($this->name)) { - $parentClassMethod = $parentClass->getMethod($this->name); - - // Access level changed - if (($this->isPublic() || $this->isProtected()) && $parentClassMethod->is(self::ACCESS_LEVEL_CHANGED | InternalReflectionMethod::IS_PRIVATE)) { - $this->modifiers |= self::ACCESS_LEVEL_CHANGED; - } - - // Implemented abstract - if ($parentClassMethod->isAbstract() && !$this->isAbstract()) { - $this->modifiers |= self::IS_IMPLEMENTED_ABSTRACT; - } - } else { - // Check if it is an implementation of an interface method - foreach ($declaringClass->getInterfaces() as $interface) { - if ($interface->hasOwnMethod($this->name)) { - $this->modifiers |= self::IS_IMPLEMENTED_ABSTRACT; - break; - } - } - } - - // Set if modifiers definition is complete - $this->modifiersComplete = $this->isComplete() || (($this->modifiers & self::IS_IMPLEMENTED_ABSTRACT) && ($this->modifiers & self::ACCESS_LEVEL_CHANGED)); - } - - return $this->modifiers; - } - - /** - * Returns if the method is abstract. - * - * @return boolean - */ - public function isAbstract() - { - return (bool) ($this->modifiers & InternalReflectionMethod::IS_ABSTRACT); - } - - /** - * Returns if the method is final. - * - * @return boolean - */ - public function isFinal() - { - return (bool) ($this->modifiers & InternalReflectionMethod::IS_FINAL); - } - - /** - * Returns if the method is private. - * - * @return boolean - */ - public function isPrivate() - { - return (bool) ($this->modifiers & InternalReflectionMethod::IS_PRIVATE); - } - - /** - * Returns if the method is protected. - * - * @return boolean - */ - public function isProtected() - { - return (bool) ($this->modifiers & InternalReflectionMethod::IS_PROTECTED); - } - - /** - * Returns if the method is public. - * - * @return boolean - */ - public function isPublic() - { - return (bool) ($this->modifiers & InternalReflectionMethod::IS_PUBLIC); - } - - /** - * Returns if the method is static. - * - * @return boolean - */ - public function isStatic() - { - return (bool) ($this->modifiers & InternalReflectionMethod::IS_STATIC); - } - - /** - * Shortcut for isPublic(), ... methods that allows or-ed modifiers. - * - * The {@see getModifiers()} method is called only when really necessary making this - * a more efficient way of doing - * - * if ($method->getModifiers() & $filter) { - * ... - * } - * - * - * @param integer $filter Filter - * @return boolean - */ - public function is($filter = null) - { - // See self::ACCESS_LEVEL_CHANGED | self::IS_IMPLEMENTED_ABSTRACT - static $computedModifiers = 0x808; - - if (null === $filter || ($this->modifiers & $filter)) { - return true; - } elseif (($filter & $computedModifiers) && !$this->modifiersComplete) { - return (bool) ($this->getModifiers() & $filter); - } - - return false; - } - - /** - * Returns if the method is a constructor. - * - * @return boolean - */ - public function isConstructor() - { - return (bool) ($this->modifiers & self::IS_CONSTRUCTOR); - } - - /** - * Returns if the method is a destructor. - * - * @return boolean - */ - public function isDestructor() - { - return (bool) ($this->modifiers & self::IS_DESTRUCTOR); - } - - /** - * Returns the method prototype. - * - * @return \TokenReflection\ReflectionMethod - * @throws \TokenReflection\Exception\RuntimeException If the method has no prototype. - */ - public function getPrototype() - { - if (null === $this->prototype) { - $prototype = null; - - $declaring = $this->getDeclaringClass(); - if (($parent = $declaring->getParentClass()) && $parent->hasMethod($this->name)) { - $method = $parent->getMethod($this->name); - - if (!$method->isPrivate()) { - try { - $prototype = $method->getPrototype(); - } catch (Exception\RuntimeException $e) { - $prototype = $method; - } - } - } - - if (null === $prototype) { - foreach ($declaring->getOwnInterfaces() as $interface) { - if ($interface->hasMethod($this->name)) { - $prototype = $interface->getMethod($this->name); - break; - } - } - } - - $this->prototype = $prototype ?: ($this->isComplete() ? false : null); - } - - if (empty($this->prototype)) { - throw new Exception\RuntimeException('Method has no prototype.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - return $this->prototype; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return sprintf('%s::%s', $this->declaringClassName ?: $this->declaringTraitName, parent::getPrettyName()); - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - $internal = ''; - $overwrite = ''; - $prototype = ''; - - $declaringClassParent = $this->getDeclaringClass()->getParentClass(); - try { - $prototype = ', prototype ' . $this->getPrototype()->getDeclaringClassName(); - } catch (Exception\RuntimeException $e) { - if ($declaringClassParent && $declaringClassParent->isInternal()) { - $internal = 'internal:' . $parentClass->getExtensionName(); - } - } - - if ($declaringClassParent && $declaringClassParent->hasMethod($this->name)) { - $parentMethod = $declaringClassParent->getMethod($this->name); - $overwrite = ', overwrites ' . $parentMethod->getDeclaringClassName(); - } - - if ($this->isConstructor()) { - $cdtor = ', ctor'; - } elseif ($this->isDestructor()) { - $cdtor = ', dtor'; - } else { - $cdtor = ''; - } - - $parameters = ''; - if ($this->getNumberOfParameters() > 0) { - $buffer = ''; - foreach ($this->getParameters() as $parameter) { - $buffer .= "\n " . $parameter->__toString(); - } - $parameters = sprintf( - "\n\n - Parameters [%d] {%s\n }", - $this->getNumberOfParameters(), - $buffer - ); - } - // @todo support inherits - return sprintf( - "%sMethod [ <%s%s%s%s> %s%s%s%s%s%s method %s%s ] {\n @@ %s %d - %d%s\n}\n", - $this->getDocComment() ? $this->getDocComment() . "\n" : '', - !empty($internal) ? $internal : 'user', - $overwrite, - $prototype, - $cdtor, - $this->isAbstract() ? 'abstract ' : '', - $this->isFinal() ? 'final ' : '', - $this->isStatic() ? 'static ' : '', - $this->isPublic() ? 'public' : '', - $this->isPrivate() ? 'private' : '', - $this->isProtected() ? 'protected' : '', - $this->returnsReference() ? '&' : '', - $this->getName(), - $this->getFileName(), - $this->getStartLine(), - $this->getEndLine(), - $parameters - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string|object $class Class name or class instance - * @param string $method Method name - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $class, $method, $return = false) - { - $className = is_object($class) ? get_class($class) : $class; - $methodName = $method; - - $class = $broker->getClass($className); - if ($class instanceof Invalid\ReflectionClass) { - throw new Exception\RuntimeException('Class is invalid.', Exception\RuntimeException::UNSUPPORTED); - } elseif ($class instanceof Dummy\ReflectionClass) { - throw new Exception\RuntimeException(sprintf('Class %s does not exist.', $className), Exception\RuntimeException::DOES_NOT_EXIST); - } - $method = $class->getMethod($methodName); - - if ($return) { - return $method->__toString(); - } - - echo $method->__toString(); - } - - /** - * Calls the method on an given instance. - * - * @param object $object Class instance - * @param mixed $args - * @return mixed - */ - public function invoke($object, $args) - { - $params = func_get_args(); - return $this->invokeArgs(array_shift($params), $params); - } - - /** - * Calls the method on an given object. - * - * @param object $object Class instance - * @param array $args Method parameter values - * @return mixed - * @throws \TokenReflection\Exception\RuntimeException If it is not possible to invoke the method. - */ - public function invokeArgs($object, array $args = array()) - { - $declaringClass = $this->getDeclaringClass(); - if (!$declaringClass->isInstance($object)) { - throw new Exception\RuntimeException(sprintf('Expected instance of or subclass of "%s".', $this->declaringClassName), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - if ($this->isPublic()) { - return call_user_func_array(array($object, $this->getName()), $args); - } elseif ($this->isAccessible()) { - $refClass = new InternalReflectionClass($object); - $refMethod = $refClass->getMethod($this->name); - - $refMethod->setAccessible(true); - $value = $refMethod->invokeArgs($object, $args); - $refMethod->setAccessible(false); - - return $value; - } - - throw new Exception\RuntimeException('Only public methods can be invoked.', Exception\RuntimeException::NOT_ACCESSBILE, $this); - } - - /** - * Returns if the property is set accessible. - * - * @return boolean - */ - public function isAccessible() - { - return $this->accessible; - } - - /** - * Sets a method to be accessible or not. - * - * @param boolean $accessible - */ - public function setAccessible($accessible) - { - $this->accessible = (bool) $accessible; - } - - /** - * Returns if the definition is complete. - * - * Technically returns if the declaring class definition is complete. - * - * @return boolean - */ - private function isComplete() - { - return $this->getDeclaringClass()->isComplete(); - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->getDeclaringClass()->getNamespaceAliases(); - } - - /** - * Returns the function/method as closure. - * - * @param object $object Object - * @return \Closure - */ - public function getClosure($object) - { - $declaringClass = $this->getDeclaringClass(); - if (!$declaringClass->isInstance($object)) { - throw new Exception\RuntimeException(sprintf('Expected instance of or subclass of "%s".', $this->declaringClassName), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $that = $this; - return function() use ($object, $that) { - return $that->invokeArgs($object, func_get_args()); - }; - } - - /** - * Creates a method alias of the given name and access level for the given class. - * - * @param \TokenReflection\ReflectionClass $parent New parent class - * @param string $name New method name - * @param integer $accessLevel New access level - * @return \TokenReflection\ReflectionMethod - * @throws \TokenReflection\Exception\RuntimeException If an invalid method access level was found. - */ - public function alias(ReflectionClass $parent, $name = null, $accessLevel = null) - { - static $possibleLevels = array(InternalReflectionMethod::IS_PUBLIC => true, InternalReflectionMethod::IS_PROTECTED => true, InternalReflectionMethod::IS_PRIVATE => true); - - $method = clone $this; - - $method->declaringClassName = $parent->getName(); - if (null !== $name) { - $method->originalName = $this->name; - $method->name = $name; - } - if (null !== $accessLevel) { - if (!isset($possibleLevels[$accessLevel])) { - throw new Exception\RuntimeException(sprintf('Invalid method access level: "%s".', $accessLevel), Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - $method->modifiers &= ~(InternalReflectionMethod::IS_PUBLIC | InternalReflectionMethod::IS_PROTECTED | InternalReflectionMethod::IS_PRIVATE); - $method->modifiers |= $accessLevel; - - $method->originalModifiers = $this->getModifiers(); - } - - foreach ($this->parameters as $parameterName => $parameter) { - $method->parameters[$parameterName] = $parameter->alias($method); - } - - return $method; - } - - /** - * Returns the original name when importing from a trait. - * - * @return string|null - */ - public function getOriginalName() - { - return $this->originalName; - } - - /** - * Returns the original method when importing from a trait. - * - * @return \TokenReflection\IReflectionMethod|null - */ - public function getOriginal() - { - return $this->original; - } - - /** - * Returns the original modifiers value when importing from a trait. - * - * @return integer|null - */ - public function getOriginalModifiers() - { - return $this->originalModifiers; - } - - /** - * Returns the defining trait. - * - * @return \TokenReflection\IReflectionClass|null - */ - public function getDeclaringTrait() - { - return null === $this->declaringTraitName ? null : $this->getBroker()->getClass($this->declaringTraitName); - } - - /** - * Returns the declaring trait name. - * - * @return string|null - */ - public function getDeclaringTraitName() - { - return $this->declaringTraitName; - } - - /** - * Processes the parent reflection object. - * - * @param \TokenReflection\IReflection $parent Parent reflection object - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionElement - * @throws \TokenReflection\Exception\ParseException If an invalid parent reflection object was provided. - */ - protected function processParent(IReflection $parent, Stream $tokenStream) - { - if (!$parent instanceof ReflectionClass) { - throw new Exception\ParseException($this, $tokenStream, 'The parent object has to be an instance of TokenReflection\ReflectionClass.', Exception\ParseException::INVALID_PARENT); - } - - $this->declaringClassName = $parent->getName(); - if ($parent->isTrait()) { - $this->declaringTraitName = $parent->getName(); - } - return parent::processParent($parent, $tokenStream); - } - - /** - * Parses reflected element metadata from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionMethod - * @throws \TokenReflection\Exception\Parse If the class could not be parsed. - */ - protected function parse(Stream $tokenStream, IReflection $parent) - { - return $this - ->parseBaseModifiers($tokenStream) - ->parseReturnsReference($tokenStream) - ->parseName($tokenStream) - ->parseInternalModifiers($parent); - } - - /** - * Parses base method modifiers (abstract, final, public, ...). - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionMethod - */ - private function parseBaseModifiers(Stream $tokenStream) - { - while (true) { - switch ($tokenStream->getType()) { - case T_ABSTRACT: - $this->modifiers |= InternalReflectionMethod::IS_ABSTRACT; - break; - case T_FINAL: - $this->modifiers |= InternalReflectionMethod::IS_FINAL; - break; - case T_PUBLIC: - $this->modifiers |= InternalReflectionMethod::IS_PUBLIC; - break; - case T_PRIVATE: - $this->modifiers |= InternalReflectionMethod::IS_PRIVATE; - break; - case T_PROTECTED: - $this->modifiers |= InternalReflectionMethod::IS_PROTECTED; - break; - case T_STATIC: - $this->modifiers |= InternalReflectionMethod::IS_STATIC; - break; - case T_FUNCTION: - case null: - break 2; - default: - break; - } - - $tokenStream->skipWhitespaces(); - } - - if (!($this->modifiers & (InternalReflectionMethod::IS_PRIVATE | InternalReflectionMethod::IS_PROTECTED))) { - $this->modifiers |= InternalReflectionMethod::IS_PUBLIC; - } - - return $this; - } - - /** - * Parses internal PHP method modifiers (abstract, final, public, ...). - * - * @param \TokenReflection\ReflectionClass $class Parent class - * @return \TokenReflection\ReflectionMethod - */ - private function parseInternalModifiers(ReflectionClass $class) - { - $name = strtolower($this->name); - // In PHP 5.3.3+ the ctor can be named only __construct in namespaced classes - if ('__construct' === $name || ((!$class->inNamespace() || PHP_VERSION_ID < 50303) && strtolower($class->getShortName()) === $name)) { - $this->modifiers |= self::IS_CONSTRUCTOR; - } elseif ('__destruct' === $name) { - $this->modifiers |= self::IS_DESTRUCTOR; - } elseif ('__clone' === $name) { - $this->modifiers |= self::IS_CLONE; - } - - if ($class->isInterface()) { - $this->modifiers |= InternalReflectionMethod::IS_ABSTRACT; - } else { - // Can be called statically, see http://svn.php.net/viewvc/php/php-src/branches/PHP_5_3/Zend/zend_API.c?revision=309853&view=markup#l1795 - static $notAllowed = array('__clone' => true, '__tostring' => true, '__get' => true, '__set' => true, '__isset' => true, '__unset' => true); - if (!$this->isStatic() && !$this->isConstructor() && !$this->isDestructor() && !isset($notAllowed[$name])) { - $this->modifiers |= self::IS_ALLOWED_STATIC; - } - } - - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionNamespace.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionNamespace.php deleted file mode 100644 index 527ca0e51e7..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionNamespace.php +++ /dev/null @@ -1,558 +0,0 @@ -name = $name; - $this->broker = $broker; - } - - /** - * Returns the name. - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Returns if the namespace is internal. - * - * Always false. - * - * @return boolean - */ - public function isInternal() - { - return false; - } - - /** - * Returns if the namespace is user defined. - * - * Always true. - * - * @return boolean - */ - public function isUserDefined() - { - return true; - } - - /** - * Returns if the current reflection comes from a tokenized source. - * - * @return boolean - */ - public function isTokenized() - { - return true; - } - - /** - * Returns if the namespace contains a class of the given name. - * - * @param string $className Class name - * @return boolean - */ - public function hasClass($className) - { - $className = ltrim($className, '\\'); - if (false === strpos($className, '\\') && self::NO_NAMESPACE_NAME !== $this->getName()) { - $className = $this->getName() . '\\' . $className; - } - - return isset($this->classes[$className]); - } - - /** - * Return a class reflection. - * - * @param string $className Class name - * @return \TokenReflection\ReflectionClass - * @throws \TokenReflection\Exception\RuntimeException If the requested class reflection does not exist. - */ - public function getClass($className) - { - $className = ltrim($className, '\\'); - if (false === strpos($className, '\\') && self::NO_NAMESPACE_NAME !== $this->getName()) { - $className = $this->getName() . '\\' . $className; - } - - if (!isset($this->classes[$className])) { - throw new Exception\RuntimeException(sprintf('Class "%s" does not exist.', $className), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - return $this->classes[$className]; - } - - /** - * Returns class reflections. - * - * @return array - */ - public function getClasses() - { - return $this->classes; - } - - /** - * Returns class names (FQN). - * - * @return array - */ - public function getClassNames() - { - return array_keys($this->classes); - } - - /** - * Returns class unqualified names (UQN). - * - * @return array - */ - public function getClassShortNames() - { - return array_map(function(IReflectionClass $class) { - return $class->getShortName(); - }, $this->classes); - } - - /** - * Returns if the namespace contains a constant of the given name. - * - * @param string $constantName Constant name - * @return boolean - */ - public function hasConstant($constantName) - { - $constantName = ltrim($constantName, '\\'); - if (false === strpos($constantName, '\\') && self::NO_NAMESPACE_NAME !== $this->getName()) { - $constantName = $this->getName() . '\\' . $constantName; - } - - return isset($this->constants[$constantName]); - } - - /** - * Returns a constant reflection. - * - * @param string $constantName Constant name - * @return \TokenReflection\ReflectionConstant - * @throws \TokenReflection\Exception\RuntimeException If the required constant does not exist. - */ - public function getConstant($constantName) - { - $constantName = ltrim($constantName, '\\'); - if (false === strpos($constantName, '\\') && self::NO_NAMESPACE_NAME !== $this->getName()) { - $constantName = $this->getName() . '\\' . $constantName; - } - - if (!isset($this->constants[$constantName])) { - throw new Exception\RuntimeException(sprintf('Constant "%s" does not exist.', $constantName), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - return $this->constants[$constantName]; - } - - /** - * Returns constant reflections. - * - * @return array - */ - public function getConstants() - { - return $this->constants; - } - - /** - * Returns constant names (FQN). - * - * @return array - */ - public function getConstantNames() - { - return array_keys($this->constants); - } - - /** - * Returns constant unqualified names (UQN). - * - * @return array - */ - public function getConstantShortNames() - { - return array_map(function(IReflectionConstant $constant) { - return $constant->getShortName(); - }, $this->constants); - } - - /** - * Returns if the namespace contains a function of the given name. - * - * @param string $functionName Function name - * @return boolean - */ - public function hasFunction($functionName) - { - $functionName = ltrim($functionName, '\\'); - if (false === strpos($functionName, '\\') && self::NO_NAMESPACE_NAME !== $this->getName()) { - $functionName = $this->getName() . '\\' . $functionName; - } - - return isset($this->functions[$functionName]); - } - - /** - * Returns a function reflection. - * - * @param string $functionName Function name - * @return \TokenReflection\ReflectionFunction - * @throws \TokenReflection\Exception\RuntimeException If the required function does not exist. - */ - public function getFunction($functionName) - { - $functionName = ltrim($functionName, '\\'); - if (false === strpos($functionName, '\\') && self::NO_NAMESPACE_NAME !== $this->getName()) { - $functionName = $this->getName() . '\\' . $functionName; - } - - if (!isset($this->functions[$functionName])) { - throw new Exception\RuntimeException(sprintf('Function "%s" does not exist.', $functionName), Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - return $this->functions[$functionName]; - } - - /** - * Returns function reflections. - * - * @return array - */ - public function getFunctions() - { - return $this->functions; - } - - /** - * Returns function names (FQN). - * - * @return array - */ - public function getFunctionNames() - { - return array_keys($this->functions); - } - - /** - * Returns function unqualified names (UQN). - * - * @return array - */ - public function getFunctionShortNames() - { - return array_map(function(IReflectionFunction $function) { - return $function->getShortName(); - }, $this->functions); - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return $this->name; - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - $buffer = ''; - $count = 0; - foreach ($this->getClasses() as $class) { - $string = "\n " . trim(str_replace("\n", "\n ", $class->__toString()), ' '); - $string = str_replace(" \n - Parameters", "\n - Parameters", $string); - - $buffer .= $string; - $count++; - } - $classes = sprintf("\n\n - Classes [%d] {\n%s }", $count, ltrim($buffer, "\n")); - - $buffer = ''; - $count = 0; - foreach ($this->getConstants() as $constant) { - $buffer .= ' ' . $constant->__toString(); - $count++; - } - $constants = sprintf("\n\n - Constants [%d] {\n%s }", $count, $buffer); - - $buffer = ''; - $count = 0; - foreach ($this->getFunctions() as $function) { - $string = "\n " . trim(str_replace("\n", "\n ", $function->__toString()), ' '); - $string = str_replace(" \n - Parameters", "\n - Parameters", $string); - - $buffer .= $string; - $count++; - } - $functions = sprintf("\n\n - Functions [%d] {\n%s }", $count, ltrim($buffer, "\n")); - - return sprintf( - "Namespace [ namespace %s ] { %s%s%s\n}\n", - $this->getName(), - $classes, - $constants, - $functions - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string $namespace Namespace name - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $namespace, $return = false) - { - $namespaceName = $namespace; - - $namespace = $broker->getNamespace($namespaceName); - if (null === $namespace) { - throw new Exception\RuntimeException(sprintf('Namespace %s does not exist.', $namespaceName), Exception\RuntimeException::DOES_NOT_EXIST); - } - - if ($return) { - return $namespace->__toString(); - } - - echo $namespace->__toString(); - } - - /** - * Adds a namespace part from a file. - * - * @param \TokenReflection\ReflectionFileNamespace $namespace Namespace part - * @return \TokenReflection\ReflectionNamespace - * @throws \TokenReflection\Exception\FileProcessingException If one of classes, functions or constants form the namespace are already defined - */ - public function addFileNamespace(ReflectionFileNamespace $namespace) - { - $errors = array(); - - foreach ($namespace->getClasses() as $className => $reflection) { - if ($reflection instanceof Invalid\ReflectionClass) { - $errors = array_merge($errors, $reflection->getReasons()); - } - - if (isset($this->classes[$className])) { - if (!$this->classes[$className] instanceof Invalid\ReflectionClass) { - $this->classes[$className] = new Invalid\ReflectionClass($className, $this->classes[$className]->getFileName(), $this->getBroker()); - } - - $error = new Exception\RuntimeException( - sprintf('Class %s was redeclared (previously declared in file %s).', $className, $this->classes[$className]->getFileName()), - Exception\RuntimeException::ALREADY_EXISTS, - $reflection - ); - $errors[] = $error; - $this->classes[$className]->addReason($error); - - if ($reflection instanceof Invalid\ReflectionClass) { - foreach ($reflection->getReasons() as $reason) { - $this->classes[$className]->addReason($reason); - } - } - } else { - $this->classes[$className] = $reflection; - } - } - - foreach ($namespace->getFunctions() as $functionName => $reflection) { - if ($reflection instanceof Invalid\ReflectionFunction) { - $errors = array_merge($errors, $reflection->getReasons()); - } - - if (isset($this->functions[$functionName])) { - if (!$this->functions[$functionName] instanceof Invalid\ReflectionFunction) { - $this->functions[$functionName] = new Invalid\ReflectionFunction($functionName, $this->functions[$functionName]->getFileName(), $this->getBroker()); - } - - $error = new Exception\RuntimeException( - sprintf('Function %s was redeclared (previousy declared in file %s).', $functionName, $this->functions[$functionName]->getFileName()), - Exception\RuntimeException::ALREADY_EXISTS, - $reflection - ); - $errors[] = $error; - $this->functions[$functionName]->addReason($error); - - if ($reflection instanceof Invalid\ReflectionFunction) { - foreach ($reflection->getReasons() as $reason) { - $this->functions[$functionName]->addReason($reason); - } - } - } else { - $this->functions[$functionName] = $reflection; - } - } - - foreach ($namespace->getConstants() as $constantName => $reflection) { - if ($reflection instanceof Invalid\ReflectionConstant) { - $errors = array_merge($errors, $reflection->getReasons()); - } - - if (isset($this->constants[$constantName])) { - if (!$this->constants[$constantName] instanceof Invalid\ReflectionConstant) { - $this->constants[$constantName] = new Invalid\ReflectionConstant($constantName, $this->constants[$constantName]->getFileName(), $this->getBroker()); - } - - $error = new Exception\RuntimeException( - sprintf('Constant %s was redeclared (previuosly declared in file %s).', $constantName, $this->constants[$constantName]->getFileName()), - Exception\RuntimeException::ALREADY_EXISTS, - $reflection - ); - $errors[] = $error; - $this->constants[$constantName]->addReason($error); - - if ($reflection instanceof Invalid\ReflectionConstant) { - foreach ($reflection->getReasons() as $reason) { - $this->constants[$constantName]->addReason($reason); - } - } - } else { - $this->constants[$constantName] = $reflection; - } - } - - if (!empty($errors)) { - throw new Exception\FileProcessingException($errors, null); - } - - return $this; - } - - /** - * Returns the appropriate source code part. - * - * Impossible for namespaces. - * - * @throws \TokenReflection\Exception\RuntimeException If the method is called, because it's unsupported. - */ - public function getSource() - { - throw new Exception\RuntimeException('Cannot export source code of a namespace.', Exception\RuntimeException::UNSUPPORTED, $this); - } - - /** - * Returns the reflection broker used by this reflection object. - * - * @return \TokenReflection\Broker|null - */ - public function getBroker() - { - return $this->broker; - } - - /** - * Magic __get method. - * - * @param string $key Variable name - * @return mixed - */ - final public function __get($key) - { - return ReflectionElement::get($this, $key); - } - - /** - * Magic __isset method. - * - * @param string $key Variable name - * @return boolean - */ - final public function __isset($key) - { - return ReflectionElement::exists($this, $key); - } -} \ No newline at end of file diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionParameter.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionParameter.php deleted file mode 100644 index d09945856c9..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionParameter.php +++ /dev/null @@ -1,639 +0,0 @@ -declaringClassName ? null : $this->getBroker()->getClass($this->declaringClassName); - } - - /** - * Returns the declaring class name. - * - * @return string|null - */ - public function getDeclaringClassName() - { - return $this->declaringClassName; - } - - /** - * Returns the declaring function. - * - * @return \TokenReflection\ReflectionFunctionBase - */ - public function getDeclaringFunction() - { - if (null !== $this->declaringClassName) { - // Method parameter - $class = $this->getBroker()->getClass($this->declaringClassName); - if (null !== $class) { - return $class->getMethod($this->declaringFunctionName); - } - } else { - // Function parameter - return $this->getBroker()->getFunction($this->declaringFunctionName); - } - } - - /** - * Returns the declaring function name. - * - * @return string - */ - public function getDeclaringFunctionName() - { - return $this->declaringFunctionName; - } - - /** - * Returns the default value. - * - * @return mixed - * @throws \TokenReflection\Exception\RuntimeException If the property is not optional. - * @throws \TokenReflection\Exception\RuntimeException If the property has no default value. - */ - public function getDefaultValue() - { - if (!$this->isOptional()) { - throw new Exception\RuntimeException('Property is not optional.', Exception\RuntimeException::UNSUPPORTED, $this); - } - - if (is_array($this->defaultValueDefinition)) { - if (0 === count($this->defaultValueDefinition)) { - throw new Exception\RuntimeException('Property has no default value.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $this->defaultValue = Resolver::getValueDefinition($this->defaultValueDefinition, $this); - $this->defaultValueDefinition = Resolver::getSourceCode($this->defaultValueDefinition); - } - - return $this->defaultValue; - } - - /** - * Returns the part of the source code defining the parameter default value. - * - * @return string - */ - public function getDefaultValueDefinition() - { - return is_array($this->defaultValueDefinition) ? Resolver::getSourceCode($this->defaultValueDefinition) : $this->defaultValueDefinition; - } - - /** - * Retutns if a default value for the parameter is available. - * - * @return boolean - */ - public function isDefaultValueAvailable() - { - return null !== $this->getDefaultValueDefinition(); - } - - /** - * Returns the position within all parameters. - * - * @return integer - */ - public function getPosition() - { - return $this->position; - } - - /** - * Returns if the parameter expects an array. - * - * @return boolean - */ - public function isArray() - { - return $this->typeHint === self::ARRAY_TYPE_HINT; - } - - /** - * Returns if the parameter expects a callback. - * - * @return boolean - */ - public function isCallable() - { - return $this->typeHint === self::CALLABLE_TYPE_HINT; - } - - /** - * Returns the original type hint as defined in the source code. - * - * @return string|null - */ - public function getOriginalTypeHint() - { - return !$this->isArray() && !$this->isCallable() ? ltrim($this->originalTypeHint, '\\') : null; - } - - /** - * Returns reflection of the required class of the value. - * - * @return \TokenReflection\IReflectionClass|null - */ - public function getClass() - { - $name = $this->getClassName(); - if (null === $name) { - return null; - } - - return $this->getBroker()->getClass($name); - } - - /** - * Returns the required class name of the value. - * - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If the type hint class FQN could not be determined. - */ - public function getClassName() - { - if ($this->isArray() || $this->isCallable()) { - return null; - } - - if (null === $this->typeHint && null !== $this->originalTypeHint) { - if (null !== $this->declaringClassName) { - $parent = $this->getDeclaringClass(); - if (null === $parent) { - throw new Exception\RuntimeException('Could not load class reflection.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - } else { - $parent = $this->getDeclaringFunction(); - if (null === $parent || !$parent->isTokenized()) { - throw new Exception\RuntimeException('Could not load function reflection.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - } - - $lTypeHint = strtolower($this->originalTypeHint); - if ('parent' === $lTypeHint || 'self' === $lTypeHint) { - if (null === $this->declaringClassName) { - throw new Exception\RuntimeException('Parameter type hint cannot be "self" nor "parent" when not a method.', Exception\RuntimeException::UNSUPPORTED, $this); - } - - if ('parent' === $lTypeHint) { - if ($parent->isInterface() || null === $parent->getParentClassName()) { - throw new Exception\RuntimeException('Class has no parent.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $this->typeHint = $parent->getParentClassName(); - } else { - $this->typeHint = $this->declaringClassName; - } - } else { - $this->typeHint = ltrim(Resolver::resolveClassFQN($this->originalTypeHint, $parent->getNamespaceAliases(), $parent->getNamespaceName()), '\\'); - } - } - - return $this->typeHint; - } - - /** - * Returns if the the parameter allows NULL. - * - * @return boolean - */ - public function allowsNull() - { - if ($this->isArray() || $this->isCallable()) { - return 'null' === strtolower($this->getDefaultValueDefinition()); - } - - return null === $this->originalTypeHint || !empty($this->defaultValueDefinition); - } - - /** - * Returns if the parameter is optional. - * - * @return boolean - * @throws \TokenReflection\Exception\RuntimeException If it is not possible to determine if the parameter is optional. - */ - public function isOptional() - { - if (null === $this->isOptional) { - $function = $this->getDeclaringFunction(); - if (null === $function) { - throw new Exception\RuntimeException('Could not get the declaring function reflection.', Exception\RuntimeException::DOES_NOT_EXIST, $this); - } - - $this->isOptional = true; - foreach (array_slice($function->getParameters(), $this->position) as $reflectionParameter) { - if (!$reflectionParameter->isDefaultValueAvailable()) { - $this->isOptional = false; - break; - } - } - } - - return $this->isOptional; - } - - /** - * Returns if the parameter value is passed by reference. - * - * @return boolean - */ - public function isPassedByReference() - { - return $this->passedByReference; - } - - /** - * Returns if the paramter value can be passed by value. - * - * @return boolean - */ - public function canBePassedByValue() - { - return !$this->isPassedByReference(); - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return str_replace('()', '($' . $this->name . ')', $this->getDeclaringFunction()->getPrettyName()); - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - if ($this->getClass()) { - $hint = $this->getClassName(); - } elseif ($this->isArray()) { - $hint = self::ARRAY_TYPE_HINT; - } elseif ($this->isCallable()) { - $hint = self::CALLABLE_TYPE_HINT; - } else { - $hint = ''; - } - - if (!empty($hint) && $this->allowsNull()) { - $hint .= ' or NULL'; - } - - if ($this->isDefaultValueAvailable()) { - $default = ' = '; - if (null === $this->getDefaultValue()) { - $default .= 'NULL'; - } elseif (is_array($this->getDefaultValue())) { - $default .= 'Array'; - } elseif (is_bool($this->getDefaultValue())) { - $default .= $this->getDefaultValue() ? 'true' : 'false'; - } elseif (is_string($this->getDefaultValue())) { - $default .= sprintf("'%s'", str_replace("'", "\\'", $this->getDefaultValue())); - } else { - $default .= $this->getDefaultValue(); - } - } else { - $default = ''; - } - - return sprintf( - 'Parameter #%d [ <%s> %s%s$%s%s ]', - $this->getPosition(), - $this->isOptional() ? 'optional' : 'required', - $hint ? $hint . ' ' : '', - $this->isPassedByReference() ? '&' : '', - $this->getName(), - $default - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string $function Function name - * @param string $parameter Parameter name - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $function, $parameter, $return = false) - { - $functionName = $function; - $parameterName = $parameter; - - $function = $broker->getFunction($functionName); - if (null === $function) { - throw new Exception\RuntimeException(sprintf('Function %s() does not exist.', $functionName), Exception\RuntimeException::DOES_NOT_EXIST); - } - $parameter = $function->getParameter($parameterName); - - if ($return) { - return $parameter->__toString(); - } - - echo $parameter->__toString(); - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->getDeclaringFunction()->getNamespaceAliases(); - } - - /** - * Creates a parameter alias for the given method. - * - * @param \TokenReflection\ReflectionMethod $parent New parent method - * @return \TokenReflection\ReflectionParameter - */ - public function alias(ReflectionMethod $parent) - { - $parameter = clone $this; - - $parameter->declaringClassName = $parent->getDeclaringClassName(); - $parameter->declaringFunctionName = $parent->getName(); - - return $parameter; - } - - /** - * Processes the parent reflection object. - * - * @param \TokenReflection\IReflection $parent Parent reflection object - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionElement - * @throws \TokenReflection\Exception\ParseException If an invalid parent reflection object was provided. - */ - protected function processParent(IReflection $parent, Stream $tokenStream) - { - if (!$parent instanceof ReflectionFunctionBase) { - throw new Exception\ParseException($this, $tokenStream, 'The parent object has to be an instance of TokenReflection\ReflectionFunctionBase.', Exception\ParseException::INVALID_PARENT); - } - - // Declaring function name - $this->declaringFunctionName = $parent->getName(); - - // Position - $this->position = count($parent->getParameters()); - - // Declaring class name - if ($parent instanceof ReflectionMethod) { - $this->declaringClassName = $parent->getDeclaringClassName(); - } - - return parent::processParent($parent, $tokenStream); - } - - /** - * Parses reflected element metadata from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionParameter - */ - protected function parse(Stream $tokenStream, IReflection $parent) - { - return $this - ->parseTypeHint($tokenStream) - ->parsePassedByReference($tokenStream) - ->parseName($tokenStream) - ->parseDefaultValue($tokenStream); - } - - /** - * Parses the type hint. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionParameter - * @throws \TokenReflection\Exception\ParseException If the type hint class name could not be determined. - */ - private function parseTypeHint(Stream $tokenStream) - { - $type = $tokenStream->getType(); - - if (T_ARRAY === $type) { - $this->typeHint = self::ARRAY_TYPE_HINT; - $this->originalTypeHint = self::ARRAY_TYPE_HINT; - $tokenStream->skipWhitespaces(true); - } elseif (T_CALLABLE === $type) { - $this->typeHint = self::CALLABLE_TYPE_HINT; - $this->originalTypeHint = self::CALLABLE_TYPE_HINT; - $tokenStream->skipWhitespaces(true); - } elseif (T_STRING === $type || T_NS_SEPARATOR === $type) { - $className = ''; - do { - $className .= $tokenStream->getTokenValue(); - - $tokenStream->skipWhitespaces(true); - $type = $tokenStream->getType(); - } while (T_STRING === $type || T_NS_SEPARATOR === $type); - - if ('' === ltrim($className, '\\')) { - throw new Exception\ParseException($this, $tokenStream, sprintf('Invalid class name definition: "%s".', $className), Exception\ParseException::LOGICAL_ERROR); - } - - $this->originalTypeHint = $className; - } - - return $this; - } - - /** - * Parses if parameter value is passed by reference. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionParameter - */ - private function parsePassedByReference(Stream $tokenStream) - { - if ($tokenStream->is('&')) { - $this->passedByReference = true; - $tokenStream->skipWhitespaces(true); - } - - return $this; - } - - /** - * Parses the constant name. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionParameter - * @throws \TokenReflection\Exception\ParseException If the parameter name could not be determined. - */ - protected function parseName(Stream $tokenStream) - { - if (!$tokenStream->is(T_VARIABLE)) { - throw new Exception\ParseException($this, $tokenStream, 'The parameter name could not be determined.', Exception\ParseException::UNEXPECTED_TOKEN); - } - - $this->name = substr($tokenStream->getTokenValue(), 1); - - $tokenStream->skipWhitespaces(true); - - return $this; - } - - /** - * Parses the parameter default value. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionParameter - * @throws \TokenReflection\Exception\ParseException If the default value could not be determined. - */ - private function parseDefaultValue(Stream $tokenStream) - { - if ($tokenStream->is('=')) { - $tokenStream->skipWhitespaces(true); - - $level = 0; - while (null !== ($type = $tokenStream->getType())) { - switch ($type) { - case ')': - if (0 === $level) { - break 2; - } - case '}': - case ']': - $level--; - break; - case '(': - case '{': - case '[': - $level++; - break; - case ',': - if (0 === $level) { - break 2; - } - break; - default: - break; - } - - $this->defaultValueDefinition[] = $tokenStream->current(); - $tokenStream->next(); - } - - if (')' !== $type && ',' !== $type) { - throw new Exception\ParseException($this, $tokenStream, 'The property default value is not terminated properly. Expected "," or ")".', Exception\ParseException::UNEXPECTED_TOKEN); - } - } - - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/ReflectionProperty.php b/apigen/libs/TokenReflection/TokenReflection/ReflectionProperty.php deleted file mode 100644 index f85a8cb11c4..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/ReflectionProperty.php +++ /dev/null @@ -1,572 +0,0 @@ -getBroker()->getClass($this->declaringClassName); - } - - /** - * Returns the name of the declaring class. - * - * @return string - */ - public function getDeclaringClassName() - { - return $this->declaringClassName; - } - - /** - * Returns the property default value. - * - * @return mixed - */ - public function getDefaultValue() - { - if (is_array($this->defaultValueDefinition)) { - $this->defaultValue = Resolver::getValueDefinition($this->defaultValueDefinition, $this); - $this->defaultValueDefinition = Resolver::getSourceCode($this->defaultValueDefinition); - } - - return $this->defaultValue; - } - - /** - * Returns the part of the source code defining the property default value. - * - * @return string - */ - public function getDefaultValueDefinition() - { - return is_array($this->defaultValueDefinition) ? Resolver::getSourceCode($this->defaultValueDefinition) : $this->defaultValueDefinition; - } - - /** - * Returns the property value for a particular class instance. - * - * @param object $object - * @return mixed - * @throws \TokenReflection\Exception\RuntimeException If it is not possible to return the property value. - */ - public function getValue($object) - { - $declaringClass = $this->getDeclaringClass(); - if (!$declaringClass->isInstance($object)) { - throw new Exception\RuntimeException('The given class is not an instance or subclass of the current class.', Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - if ($this->isPublic()) { - return $object->{$this->name}; - } elseif ($this->isAccessible()) { - $refClass = new InternalReflectionClass($object); - $refProperty = $refClass->getProperty($this->name); - - $refProperty->setAccessible(true); - $value = $refProperty->getValue($object); - $refProperty->setAccessible(false); - - return $value; - } - - throw new Exception\RuntimeException('Only public and accessible properties can return their values.', Exception\RuntimeException::NOT_ACCESSBILE, $this); - } - - /** - * Returns if the property was created at compile time. - * - * All properties in the source code are. - * - * @return boolean - */ - public function isDefault() - { - return true; - } - - /** - * Returns property modifiers. - * - * @return integer - */ - public function getModifiers() - { - if (false === $this->modifiersComplete) { - $declaringClass = $this->getDeclaringClass(); - $declaringClassParent = $declaringClass->getParentClass(); - - if ($declaringClassParent && $declaringClassParent->hasProperty($this->name)) { - $property = $declaringClassParent->getProperty($this->name); - if (($this->isPublic() && !$property->isPublic()) || ($this->isProtected() && $property->isPrivate())) { - $this->modifiers |= self::ACCESS_LEVEL_CHANGED; - } - } - - $this->modifiersComplete = ($this->modifiers & self::ACCESS_LEVEL_CHANGED) || $declaringClass->isComplete(); - } - - return $this->modifiers; - } - - /** - * Returns if the property is private. - * - * @return boolean - */ - public function isPrivate() - { - return (bool) ($this->modifiers & InternalReflectionProperty::IS_PRIVATE); - } - - /** - * Returns if the property is protected. - * - * @return boolean - */ - public function isProtected() - { - return (bool) ($this->modifiers & InternalReflectionProperty::IS_PROTECTED); - } - - /** - * Returns if the property is public. - * - * @return boolean - */ - public function isPublic() - { - return (bool) ($this->modifiers & InternalReflectionProperty::IS_PUBLIC); - } - - /** - * Returns if the poperty is static. - * - * @return boolean - */ - public function isStatic() - { - return (bool) ($this->modifiers & InternalReflectionProperty::IS_STATIC); - } - - /** - * Returns the string representation of the reflection object. - * - * @return string - */ - public function __toString() - { - return sprintf( - "Property [ %s%s%s%s%s\$%s ]\n", - $this->isStatic() ? '' : ' ', - $this->isPublic() ? 'public ' : '', - $this->isPrivate() ? 'private ' : '', - $this->isProtected() ? 'protected ' : '', - $this->isStatic() ? 'static ' : '', - $this->getName() - ); - } - - /** - * Exports a reflected object. - * - * @param \TokenReflection\Broker $broker Broker instance - * @param string|object $class Class name or class instance - * @param string $property Property name - * @param boolean $return Return the export instead of outputting it - * @return string|null - * @throws \TokenReflection\Exception\RuntimeException If requested parameter doesn't exist. - */ - public static function export(Broker $broker, $class, $property, $return = false) - { - $className = is_object($class) ? get_class($class) : $class; - $propertyName = $property; - - $class = $broker->getClass($className); - if ($class instanceof Invalid\ReflectionClass) { - throw new Exception\RuntimeException('Class is invalid.', Exception\RuntimeException::UNSUPPORTED); - } elseif ($class instanceof Dummy\ReflectionClass) { - throw new Exception\RuntimeException(sprintf('Class %s does not exist.', $className), Exception\RuntimeException::DOES_NOT_EXIST); - } - $property = $class->getProperty($propertyName); - - if ($return) { - return $property->__toString(); - } - - echo $property->__toString(); - } - - /** - * Returns if the property is set accessible. - * - * @return boolean - */ - public function isAccessible() - { - return $this->accessible; - } - - /** - * Sets a property to be accessible or not. - * - * @param boolean $accessible If the property should be accessible. - */ - public function setAccessible($accessible) - { - $this->accessible = (bool) $accessible; - } - - /** - * Sets the property default value. - * - * @param mixed $value - */ - public function setDefaultValue($value) - { - $this->defaultValue = $value; - $this->defaultValueDefinition = var_export($value, true); - } - - /** - * Sets value of a property for a particular class instance. - * - * @param object $object Class instance - * @param mixed $value Poperty value - * @throws \TokenReflection\Exception\RuntimeException If it is not possible to set the property value. - */ - public function setValue($object, $value) - { - $declaringClass = $this->getDeclaringClass(); - if (!$declaringClass->isInstance($object)) { - throw new Exception\RuntimeException('Instance of or subclass expected.', Exception\RuntimeException::INVALID_ARGUMENT, $this); - } - - if ($this->isPublic()) { - $object->{$this->name} = $value; - } elseif ($this->isAccessible()) { - $refClass = new InternalReflectionClass($object); - $refProperty = $refClass->getProperty($this->name); - - $refProperty->setAccessible(true); - $refProperty->setValue($object, $value); - $refProperty->setAccessible(false); - - if ($this->isStatic()) { - $this->setDefaultValue($value); - } - } else { - throw new Exception\RuntimeException('Only public and accessible properties can be set.', Exception\RuntimeException::NOT_ACCESSBILE, $this); - } - } - - /** - * Returns imported namespaces and aliases from the declaring namespace. - * - * @return array - */ - public function getNamespaceAliases() - { - return $this->getDeclaringClass()->getNamespaceAliases(); - } - - /** - * Creates a property alias for the given class. - * - * @param \TokenReflection\ReflectionClass $parent New parent class - * @return \TokenReflection\ReflectionProperty - */ - public function alias(ReflectionClass $parent) - { - $property = clone $this; - $property->declaringClassName = $parent->getName(); - return $property; - } - - /** - * Returns the defining trait. - * - * @return \TokenReflection\IReflectionClass|null - */ - public function getDeclaringTrait() - { - return null === $this->declaringTraitName ? null : $this->getBroker()->getClass($this->declaringTraitName); - } - - /** - * Returns the declaring trait name. - * - * @return string|null - */ - public function getDeclaringTraitName() - { - return $this->declaringTraitName; - } - - /** - * Returns an element pretty (docblock compatible) name. - * - * @return string - */ - public function getPrettyName() - { - return sprintf('%s::$%s', $this->declaringClassName ?: $this->declaringTraitName, $this->name); - } - - /** - * Processes the parent reflection object. - * - * @param \TokenReflection\IReflection $parent Parent reflection object - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionElement - * @throws \TokenReflection\Exception\Parse If an invalid parent reflection object was provided. - */ - protected function processParent(IReflection $parent, Stream $tokenStream) - { - if (!$parent instanceof ReflectionClass) { - throw new Exception\ParseException($this, $tokenStream, 'The parent object has to be an instance of TokenReflection\ReflectionClass.', Exception\ParseException::INVALID_PARENT); - } - - $this->declaringClassName = $parent->getName(); - if ($parent->isTrait()) { - $this->declaringTraitName = $parent->getName(); - } - return parent::processParent($parent, $tokenStream); - } - - /** - * Parses reflected element metadata from the token stream. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\IReflection $parent Parent reflection object - * @return \TokenReflection\ReflectionProperty - */ - protected function parse(Stream $tokenStream, IReflection $parent) - { - $this->parseModifiers($tokenStream, $parent); - - if (false === $this->docComment->getDocComment()) { - $this->parseDocComment($tokenStream, $parent); - } - - return $this->parseName($tokenStream) - ->parseDefaultValue($tokenStream); - } - - /** - * Parses class modifiers (abstract, final) and class type (class, interface). - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @param \TokenReflection\ReflectionClass $class Defining class - * @return \TokenReflection\ReflectionClass - * @throws \TokenReflection\Exception\ParseException If the modifiers value cannot be determined. - */ - private function parseModifiers(Stream $tokenStream, ReflectionClass $class) - { - while (true) { - switch ($tokenStream->getType()) { - case T_PUBLIC: - case T_VAR: - $this->modifiers |= InternalReflectionProperty::IS_PUBLIC; - break; - case T_PROTECTED: - $this->modifiers |= InternalReflectionProperty::IS_PROTECTED; - break; - case T_PRIVATE: - $this->modifiers |= InternalReflectionProperty::IS_PRIVATE; - break; - case T_STATIC: - $this->modifiers |= InternalReflectionProperty::IS_STATIC; - break; - default: - break 2; - } - - $tokenStream->skipWhitespaces(true); - } - - if (InternalReflectionProperty::IS_STATIC === $this->modifiers) { - $this->modifiers |= InternalReflectionProperty::IS_PUBLIC; - } elseif (0 === $this->modifiers) { - $parentProperties = $class->getOwnProperties(); - if (empty($parentProperties)) { - throw new Exception\ParseException($this, $tokenStream, 'No access level defined and no previous defining class property present.', Exception\ParseException::LOGICAL_ERROR); - } - - $sibling = array_pop($parentProperties); - if ($sibling->isPublic()) { - $this->modifiers = InternalReflectionProperty::IS_PUBLIC; - } elseif ($sibling->isPrivate()) { - $this->modifiers = InternalReflectionProperty::IS_PRIVATE; - } elseif ($sibling->isProtected()) { - $this->modifiers = InternalReflectionProperty::IS_PROTECTED; - } else { - throw new Exception\ParseException($this, $tokenStream, sprintf('Property sibling "%s" has no access level defined.', $sibling->getName()), Exception\Parse::PARSE_ELEMENT_ERROR); - } - - if ($sibling->isStatic()) { - $this->modifiers |= InternalReflectionProperty::IS_STATIC; - } - } - - return $this; - } - - /** - * Parses the property name. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionProperty - * @throws \TokenReflection\Exception\ParseException If the property name could not be determined. - */ - protected function parseName(Stream $tokenStream) - { - if (!$tokenStream->is(T_VARIABLE)) { - throw new Exception\ParseException($this, $tokenStream, 'The property name could not be determined.', Exception\ParseException::LOGICAL_ERROR); - } - - $this->name = substr($tokenStream->getTokenValue(), 1); - - $tokenStream->skipWhitespaces(true); - - return $this; - } - - /** - * Parses the propety default value. - * - * @param \TokenReflection\Stream\StreamBase $tokenStream Token substream - * @return \TokenReflection\ReflectionProperty - * @throws \TokenReflection\Exception\ParseException If the property default value could not be determined. - */ - private function parseDefaultValue(Stream $tokenStream) - { - $type = $tokenStream->getType(); - - if (';' === $type || ',' === $type) { - // No default value - return $this; - } - - if ('=' === $type) { - $tokenStream->skipWhitespaces(true); - } - - $level = 0; - while (null !== ($type = $tokenStream->getType())) { - switch ($type) { - case ',': - if (0 !== $level) { - break; - } - case ';': - break 2; - case ')': - case ']': - case '}': - $level--; - break; - case '(': - case '{': - case '[': - $level++; - break; - default: - break; - } - - $this->defaultValueDefinition[] = $tokenStream->current(); - $tokenStream->next(); - } - - if (',' !== $type && ';' !== $type) { - throw new Exception\ParseException($this, $tokenStream, 'The property default value is not terminated properly. Expected "," or ";".', Exception\ParseException::UNEXPECTED_TOKEN); - } - - return $this; - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Resolver.php b/apigen/libs/TokenReflection/TokenReflection/Resolver.php deleted file mode 100644 index 4d97f183df5..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Resolver.php +++ /dev/null @@ -1,288 +0,0 @@ -getNamespaceName(); - } elseif ($reflection instanceof ReflectionParameter) { - $namespace = $reflection->getDeclaringFunction()->getNamespaceName(); - } elseif ($reflection instanceof ReflectionProperty || $reflection instanceof ReflectionMethod) { - $namespace = $reflection->getDeclaringClass()->getNamespaceName(); - } else { - throw new Exception\RuntimeException('Invalid reflection object given.', Exception\RuntimeException::INVALID_ARGUMENT, $reflection); - } - - // Process __LINE__ constants; replace with the line number of the corresponding token - foreach ($tokens as $index => $token) { - if (T_LINE === $token[0]) { - $tokens[$index] = array( - T_LNUMBER, - $token[2], - $token[2] - ); - } - } - - $source = self::getSourceCode($tokens); - - $constants = self::findConstants($tokens, $reflection); - if (!empty($constants)) { - foreach (array_reverse($constants, true) as $offset => $constant) { - $value = ''; - - try { - switch ($constant) { - case '__LINE__': - throw new Exception\RuntimeException('__LINE__ constant cannot be resolved this way.', Exception\RuntimeException::UNSUPPORTED, $reflection); - case '__FILE__': - $value = $reflection->getFileName(); - break; - case '__DIR__': - $value = dirname($reflection->getFileName()); - break; - case '__FUNCTION__': - if ($reflection instanceof IReflectionParameter) { - $value = $reflection->getDeclaringFunctionName(); - } elseif ($reflection instanceof IReflectionFunctionBase) { - $value = $reflection->getName(); - } - break; - case '__CLASS__': - if ($reflection instanceof IReflectionConstant || $reflection instanceof IReflectionParameter || $reflection instanceof IReflectionProperty || $reflection instanceof IReflectionMethod) { - $value = $reflection->getDeclaringClassName() ?: ''; - } - break; - case '__TRAIT__': - if ($reflection instanceof IReflectionMethod || $reflection instanceof IReflectionProperty) { - $value = $reflection->getDeclaringTraitName() ?: ''; - } elseif ($reflection instanceof IReflectionParameter) { - $method = $reflection->getDeclaringFunction(); - if ($method instanceof IReflectionMethod) { - $value = $method->getDeclaringTraitName() ?: ''; - } - } - break; - case '__METHOD__': - if ($reflection instanceof IReflectionParameter) { - if (null !== $reflection->getDeclaringClassName()) { - $value = $reflection->getDeclaringClassName() . '::' . $reflection->getDeclaringFunctionName(); - } else { - $value = $reflection->getDeclaringFunctionName(); - } - } elseif ($reflection instanceof IReflectionConstant || $reflection instanceof IReflectionProperty) { - $value = $reflection->getDeclaringClassName() ?: ''; - } elseif ($reflection instanceof IReflectionMethod) { - $value = $reflection->getDeclaringClassName() . '::' . $reflection->getName(); - } elseif ($reflection instanceof IReflectionFunction) { - $value = $reflection->getName(); - } - break; - case '__NAMESPACE__': - if (($reflection instanceof IReflectionConstant && null !== $reflection->getDeclaringClassName()) || $reflection instanceof IReflectionProperty) { - $value = $reflection->getDeclaringClass()->getNamespaceName(); - } elseif ($reflection instanceof IReflectionParameter) { - if (null !== $reflection->getDeclaringClassName()) { - $value = $reflection->getDeclaringClass()->getNamespaceName(); - } else { - $value = $reflection->getDeclaringFunction()->getNamespaceName(); - } - } elseif ($reflection instanceof IReflectionMethod) { - $value = $reflection->getDeclaringClass()->getNamespaceName(); - } else { - $value = $reflection->getNamespaceName(); - } - break; - default: - if (0 === stripos($constant, 'self::') || 0 === stripos($constant, 'parent::')) { - // Handle self:: and parent:: definitions - - if ($reflection instanceof ReflectionConstant) { - throw new Exception\RuntimeException('Constants cannot use self:: and parent:: references.', Exception\RuntimeException::UNSUPPORTED, $reflection); - } elseif ($reflection instanceof ReflectionParameter && null === $reflection->getDeclaringClassName()) { - throw new Exception\RuntimeException('Function parameters cannot use self:: and parent:: references.', Exception\RuntimeException::UNSUPPORTED, $reflection); - } - - if (0 === stripos($constant, 'self::')) { - $className = $reflection->getDeclaringClassName(); - } else { - $declaringClass = $reflection->getDeclaringClass(); - $className = $declaringClass->getParentClassName() ?: self::CONSTANT_NOT_FOUND; - } - - $constantName = $className . substr($constant, strpos($constant, '::')); - } else { - $constantName = self::resolveClassFQN($constant, $reflection->getNamespaceAliases(), $namespace); - if ($cnt = strspn($constant, '\\')) { - $constantName = str_repeat('\\', $cnt) . $constantName; - } - } - - $reflection = $reflection->getBroker()->getConstant($constantName); - $value = $reflection->getValue(); - } - } catch (Exception\RuntimeException $e) { - $value = self::CONSTANT_NOT_FOUND; - } - - $source = substr_replace($source, var_export($value, true), $offset, strlen($constant)); - } - } - - return self::evaluate(sprintf("return %s;\n", $source)); - } - - /** - * Returns a part of the source code defined by given tokens. - * - * @param array $tokens Tokens array - * @return array - */ - final public static function getSourceCode(array $tokens) - { - if (empty($tokens)) { - return null; - } - - $source = ''; - foreach ($tokens as $token) { - $source .= $token[1]; - } - return $source; - } - - /** - * Finds constant names in the token definition. - * - * @param array $tokens Tokenized source code - * @param \TokenReflection\ReflectionElement $reflection Caller reflection - * @return array - */ - final public static function findConstants(array $tokens, ReflectionElement $reflection) - { - static $accepted = array( - T_DOUBLE_COLON => true, - T_STRING => true, - T_NS_SEPARATOR => true, - T_CLASS_C => true, - T_DIR => true, - T_FILE => true, - T_LINE => true, - T_FUNC_C => true, - T_METHOD_C => true, - T_NS_C => true, - T_TRAIT_C => true - ); - static $dontResolve = array('true' => true, 'false' => true, 'null' => true); - - // Adding a dummy token to the end - $tokens[] = array(null); - - $constants = array(); - $constant = ''; - $offset = 0; - foreach ($tokens as $token) { - if (isset($accepted[$token[0]])) { - $constant .= $token[1]; - } elseif ('' !== $constant) { - if (!isset($dontResolve[strtolower($constant)])) { - $constants[$offset - strlen($constant)] = $constant; - } - $constant = ''; - } - - if (null !== $token[0]) { - $offset += strlen($token[1]); - } - } - return $constants; - } - - /** - * Evaluates a source code. - * - * @param string $source Source code - * @return mixed - */ - final private static function evaluate($source) { - return eval($source); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Stream/FileStream.php b/apigen/libs/TokenReflection/TokenReflection/Stream/FileStream.php deleted file mode 100644 index 44e367562d2..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Stream/FileStream.php +++ /dev/null @@ -1,50 +0,0 @@ -fileName = Broker::getRealPath($fileName); - - if (false === $this->fileName) { - throw new Exception\StreamException($this, 'File does not exist.', Exception\StreamException::DOES_NOT_EXIST); - } - - $contents = @file_get_contents($this->fileName); - if (false === $contents) { - throw new Exception\StreamException($this, 'File is not readable.', Exception\StreamException::NOT_READABLE); - } - - $this->processSource($contents); - } -} \ No newline at end of file diff --git a/apigen/libs/TokenReflection/TokenReflection/Stream/StreamBase.php b/apigen/libs/TokenReflection/TokenReflection/Stream/StreamBase.php deleted file mode 100644 index 85bdcee2ba1..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Stream/StreamBase.php +++ /dev/null @@ -1,487 +0,0 @@ - true, T_WHITESPACE => true, T_DOC_COMMENT => true, T_INLINE_HTML => true, T_ENCAPSED_AND_WHITESPACE => true, T_CONSTANT_ENCAPSED_STRING => true); - - foreach ($stream as $position => $token) { - if (is_array($token)) { - if (!NATIVE_TRAITS && T_STRING === $token[0]) { - $lValue = strtolower($token[1]); - if ('trait' === $lValue) { - $token[0] = T_TRAIT; - } elseif ('insteadof' === $lValue) { - $token[0] = T_INSTEADOF; - } elseif ('__TRAIT__' === $token[1]) { - $token[0] = T_TRAIT_C; - } elseif ('callable' === $lValue) { - $token[0] = T_CALLABLE; - } - } - - $this->tokens[] = $token; - } else { - $previous = $this->tokens[$position - 1]; - $line = $previous[2]; - if (isset($checkLines[$previous[0]])) { - $line += substr_count($previous[1], "\n"); - } - - $this->tokens[] = array($token, $token, $line); - } - } - - $this->count = count($this->tokens); - } - - /** - * Returns the file name this is a part of. - * - * @return string - */ - public function getFileName() - { - return $this->fileName; - } - - /** - * Returns the original source code. - * - * @return string - */ - public function getSource() - { - return $this->getSourcePart(); - } - - /** - * Returns a part of the source code. - * - * @param mixed $start Start offset - * @param mixed $end End offset - * @return string - */ - public function getSourcePart($start = null, $end = null) - { - $start = (int) $start; - $end = null === $end ? ($this->count - 1) : (int) $end; - - $source = ''; - for ($i = $start; $i <= $end; $i++) { - $source .= $this->tokens[$i][1]; - } - return $source; - } - - /** - * Finds the position of the token of the given type. - * - * @param integer|string $type Token type - * @return \TokenReflection\Stream|boolean - */ - public function find($type) - { - $actual = $this->position; - while (isset($this->tokens[$this->position])) { - if ($type === $this->tokens[$this->position][0]) { - return $this; - } - - $this->position++; - } - - $this->position = $actual; - return false; - } - - /** - * Returns the position of the token with the matching bracket. - * - * @return \TokenReflection\Stream - * @throws \TokenReflection\Exception\RuntimeException If out of the token stream. - * @throws \TokenReflection\Exception\RuntimeException If there is no bracket at the current position. - * @throws \TokenReflection\Exception\RuntimeException If the matching bracket could not be found. - */ - public function findMatchingBracket() - { - static $brackets = array( - '(' => ')', - '{' => '}', - '[' => ']', - T_CURLY_OPEN => '}', - T_DOLLAR_OPEN_CURLY_BRACES => '}' - ); - - if (!$this->valid()) { - throw new Exception\StreamException($this, 'Out of token stream.', Exception\StreamException::READ_BEYOND_EOS); - } - - $position = $this->position; - - $bracket = $this->tokens[$this->position][0]; - - if (!isset($brackets[$bracket])) { - throw new Exception\StreamException($this, sprintf('There is no usable bracket at position "%d".', $position), Exception\StreamException::DOES_NOT_EXIST); - } - - $searching = $brackets[$bracket]; - - $level = 0; - while (isset($this->tokens[$this->position])) { - $type = $this->tokens[$this->position][0]; - if ($searching === $type) { - $level--; - } elseif ($bracket === $type || ($searching === '}' && ('{' === $type || T_CURLY_OPEN === $type || T_DOLLAR_OPEN_CURLY_BRACES === $type))) { - $level++; - } - - if (0 === $level) { - return $this; - } - - $this->position++; - } - - throw new Exception\StreamException($this, sprintf('Could not find the end bracket "%s" of the bracket at position "%d".', $searching, $position), Exception\StreamException::DOES_NOT_EXIST); - } - - /** - * Skips whitespaces and comments next to the current position. - * - * @param boolean $skipDocBlocks Skip docblocks as well - * @return \TokenReflection\Stream\StreamBase - */ - public function skipWhitespaces($skipDocBlocks = false) - { - static $skipped = array(T_WHITESPACE => true, T_COMMENT => true, T_DOC_COMMENT => true); - - do { - $this->position++; - } while (isset($this->tokens[$this->position]) && isset($skipped[$this->tokens[$this->position][0]]) && ($skipDocBlocks || $this->tokens[$this->position][0] !== T_DOC_COMMENT)); - - return $this; - } - - /** - * Returns if the token stream is at a whitespace position. - * - * @param boolean $docBlock Consider docblocks as whitespaces - * @return boolean - */ - public function isWhitespace($docBlock = false) - { - static $skipped = array(T_WHITESPACE => true, T_COMMENT => true, T_DOC_COMMENT => false); - - if (!$this->valid()) { - return false; - } - - return $docBlock ? isset($skipped[$this->getType()]) : !empty($skipped[$this->getType()]); - } - - /** - * Checks if there is a token of the given type at the given position. - * - * @param integer|string $type Token type - * @param integer $position Position; if none given, consider the current iteration position - * @return boolean - */ - public function is($type, $position = -1) - { - return $type === $this->getType($position); - } - - /** - * Returns the type of a token. - * - * @param integer $position Token position; if none given, consider the current iteration position - * @return string|integer|null - */ - public function getType($position = -1) - { - if (-1 === $position) { - $position = $this->position; - } - - return isset($this->tokens[$position]) ? $this->tokens[$position][0] : null; - } - - /** - * Returns the current token value. - * - * @param integer $position Token position; if none given, consider the current iteration position - * @return stirng - */ - public function getTokenValue($position = -1) - { - if (-1 === $position) { - $position = $this->position; - } - - return isset($this->tokens[$position]) ? $this->tokens[$position][1] : null; - } - - /** - * Returns the token type name. - * - * @param integer $position Token position; if none given, consider the current iteration position - * @return string|null - */ - public function getTokenName($position = -1) - { - $type = $this->getType($position); - if (is_string($type)) { - return $type; - } elseif (T_TRAIT === $type) { - return 'T_TRAIT'; - } elseif (T_INSTEADOF === $type) { - return 'T_INSTEADOF'; - } elseif (T_CALLABLE === $type) { - return 'T_CALLABLE'; - } - - return token_name($type); - } - - /** - * Stream serialization. - * - * @return string - */ - public function serialize() - { - return serialize(array($this->fileName, $this->tokens)); - } - - /** - * Restores the stream from the serialized state. - * - * @param string $serialized Serialized form - * @throws \TokenReflection\Exception\StreamException On deserialization error. - */ - public function unserialize($serialized) - { - $data = @unserialize($serialized); - if (false === $data) { - throw new Exception\StreamException($this, 'Could not deserialize the serialized data.', Exception\StreamException::SERIALIZATION_ERROR); - } - if (2 !== count($data) || !is_string($data[0]) || !is_array($data[1])) { - throw new Exception\StreamException($this, 'Invalid serialization data.', Exception\StreamException::SERIALIZATION_ERROR); - } - - $this->fileName = $data[0]; - $this->tokens = $data[1]; - $this->count = count($this->tokens); - $this->position = 0; - } - - /** - * Checks of there is a token with the given index. - * - * @param integer $offset Token index - * @return boolean - */ - public function offsetExists($offset) - { - return isset($this->tokens[$offset]); - } - - /** - * Removes a token. - * - * Unsupported. - * - * @param integer $offset Position - * @throws \TokenReflection\Exception\StreamException Unsupported. - */ - public function offsetUnset($offset) - { - throw new Exception\StreamException($this, 'Removing of tokens from the stream is not supported.', Exception\StreamException::UNSUPPORTED); - } - - /** - * Returns a token at the given index. - * - * @param integer $offset Token index - * @return mixed - */ - public function offsetGet($offset) - { - return isset($this->tokens[$offset]) ? $this->tokens[$offset] : null; - } - - /** - * Sets a value of a particular token. - * - * Unsupported - * - * @param integer $offset Position - * @param mixed $value Value - * @throws \TokenReflection\Exception\StreamException Unsupported. - */ - public function offsetSet($offset, $value) - { - throw new Exception\StreamException($this, 'Setting token values is not supported.', Exception\StreamException::UNSUPPORTED); - } - - /** - * Returns the current internal pointer value. - * - * @return integer - */ - public function key() - { - return $this->position; - } - - /** - * Advances the internal pointer. - * - * @return \TokenReflection\Stream - */ - public function next() - { - $this->position++; - return $this; - } - - /** - * Sets the internal pointer to zero. - * - * @return \TokenReflection\Stream - */ - public function rewind() - { - $this->position = 0; - return $this; - } - - /** - * Returns the current token. - * - * @return array|null - */ - public function current() - { - return isset($this->tokens[$this->position]) ? $this->tokens[$this->position] : null; - } - - /** - * Checks if there is a token on the current position. - * - * @return boolean - */ - public function valid() - { - return isset($this->tokens[$this->position]); - } - - /** - * Returns the number of tokens in the stream. - * - * @return integer - */ - public function count() - { - return $this->count; - } - - /** - * Sets the internal pointer to the given value. - * - * @param integer $position New position - * @return \TokenReflection\Stream - */ - public function seek($position) - { - $this->position = (int) $position; - return $this; - } - - /** - * Returns the stream source code. - * - * @return string - */ - public function __toString() - { - return $this->getSource(); - } -} diff --git a/apigen/libs/TokenReflection/TokenReflection/Stream/StringStream.php b/apigen/libs/TokenReflection/TokenReflection/Stream/StringStream.php deleted file mode 100644 index 16f7c2b18fe..00000000000 --- a/apigen/libs/TokenReflection/TokenReflection/Stream/StringStream.php +++ /dev/null @@ -1,38 +0,0 @@ -fileName = $fileName; - $this->processSource($source); - } -} \ No newline at end of file diff --git a/apigen/templates/woodocs/404.latte b/apigen/templates/woodocs/404.latte deleted file mode 100644 index 9b838179ee7..00000000000 --- a/apigen/templates/woodocs/404.latte +++ /dev/null @@ -1,23 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $robots = false} - -{block #title}Page not found{/block} - -{block #content} -
-

{include #title}

-

The requested page could not be found.

-

You have probably clicked on a link that is outdated and points to a page that does not exist any more or you have made an typing error in the address.

-

To continue please try to find requested page in the menu,{if $config->tree} take a look at the tree view of the whole project{/if} or use search field on the top.

-
-{/block} \ No newline at end of file diff --git a/apigen/templates/woodocs/@elementlist.latte b/apigen/templates/woodocs/@elementlist.latte deleted file mode 100644 index 18a70f8bbd3..00000000000 --- a/apigen/templates/woodocs/@elementlist.latte +++ /dev/null @@ -1,59 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{define #elements} - - {if $namespace}{$element->shortName}{else}{$element->name}{/if} - {!$element|shortDescription} - -{/define} - -{if $classes} -

Classes summary

- -{include #elements, elements => $classes} -
-{/if} - -{if $interfaces} -

Interfaces summary

- -{include #elements, elements => $interfaces} -
-{/if} - -{if $traits} -

Traits summary

- -{include #elements, elements => $traits} -
-{/if} - -{if $exceptions} -

Exceptions summary

- -{include #elements, elements => $exceptions} -
-{/if} - -{if $constants} -

Constants summary

- -{include #elements, elements => $constants} -
-{/if} - -{if $functions} -

Functions summary

- -{include #elements, elements => $functions} -
-{/if} diff --git a/apigen/templates/woodocs/@layout.latte b/apigen/templates/woodocs/@layout.latte deleted file mode 100644 index f89cbefefe4..00000000000 --- a/apigen/templates/woodocs/@layout.latte +++ /dev/null @@ -1,186 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{default $robots = true} -{default $active = ''} - - - - - - - - {include #title}{if 'overview' !== $active && $config->title} | {$config->title}{/if} - - {var combinedJs = 'resources/combined.js'} - - {var elementListJs = 'elementlist.js'} - - {var bootstrapCss = 'resources/bootstrap.min.css'} - - {var styleCss = 'resources/style.css'} - - - - - - - - - -
- -
- -
- - - - diff --git a/apigen/templates/woodocs/class.latte b/apigen/templates/woodocs/class.latte deleted file mode 100644 index ed90df10ccd..00000000000 --- a/apigen/templates/woodocs/class.latte +++ /dev/null @@ -1,431 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'class'} - -{block #title}{if $class->deprecated}Deprecated {/if}{if $class->interface}Interface{elseif $class->trait}Trait{else}Class{/if} {$class->name}{/block} - -{block #content} -
-

{if $class->interface}Interface{elseif $class->trait}Trait{else}Class{/if} {$class->shortName}

- - {if $class->valid} - -
- {!$class|longDescription} -
- -
-
- Extended by - {if $item->documented} - {last}{/last}{$item->name}{last}{/last} - {else}{$item->name}{/if} - {var $itemOwnInterfaces = $item->ownInterfaces} - {if $itemOwnInterfaces} implements {foreach $itemOwnInterfaces as $interface} - {$interface->name}{sep}, {/sep} - {/foreach}{/if} - {var $itemOwnTraits = $item->ownTraits} - {if $itemOwnTraits} uses {foreach $itemOwnTraits as $trait} - {$trait->name}{sep}, {/sep} - {/foreach}{/if} -
-
- - {define #children} -

- {foreach $children as $child} - {$child->name}{sep}, {/sep} - {/foreach} -

- {/define} - -
-

Direct known subclasses

- {include #children, children => $directSubClasses} -
- -
-

Indirect known subclasses

- {include #children, children => $indirectSubClasses} -
- -
-

Direct known implementers

- {include #children, children => $directImplementers} -
- -
-

Indirect known implementers

- {include #children, children => $indirectImplementers} -
- -
-

Direct Known Users

- {include #children, children => $directUsers} -
- -
-

Indirect Known Users

- {include #children, children => $indirectUsers} -
- -
- {if !$class->interface && !$class->trait && ($class->abstract || $class->final)}{if $class->abstract}Abstract{else}Final{/if}
{/if} - {if $class->internal}PHP Extension: {$class->extension->name|firstUpper}
{/if} - {if $class->inNamespace()}Namespace: {!$class->namespaceName|namespaceLinks}
{/if} - {if $class->inPackage()}Package: {!$class->packageName|packageLinks}
{/if} - - {foreach $template->annotationSort($template->annotationFilter($class->annotations)) as $annotation => $values} - {foreach $values as $value} - {$annotation|annotationBeautify}{if $value}:{/if} - {!$value|annotation:$annotation:$class}
- {/foreach} - {/foreach} - {if $class->internal}Documented at php.net{else}Located at {$class->fileName|relativePath}{/if}
-
- - {var $ownMethods = $class->ownMethods} - {var $inheritedMethods = $class->inheritedMethods} - {var $usedMethods = $class->usedMethods} - {var $ownMagicMethods = $class->ownMagicMethods} - {var $inheritedMagicMethods = $class->inheritedMagicMethods} - {var $usedMagicMethods = $class->usedMagicMethods} - - {if $ownMethods || $inheritedMethods || $usedMethods || $ownMagicMethods || $usedMagicMethods} - {define #method} - - {var $annotations = $method->annotations} - - - {if !$class->interface && $method->abstract}abstract{elseif $method->final}final{/if} {if $method->protected}protected{elseif $method->private}private{else}public{/if} {if $method->static}static{/if} - {ifset $annotations['return']}{!$annotations['return'][0]|typeLinks:$method}{/ifset} - {if $method->returnsReference()}&{/if} - - - -
- # - {block|strip} - {if $class->internal} - {$method->name}( - {else} - {$method->name}( - {/if} - {foreach $method->parameters as $parameter} - {!$parameter->typeHint|typeLinks:$method} - {if $parameter->passedByReference}& {/if}${$parameter->name}{if $parameter->defaultValueAvailable} = {!$parameter->defaultValueDefinition|highlightPHP:$class}{elseif $parameter->unlimited},…{/if}{sep}, {/sep} - {/foreach} - ){/block} - - {if $config->template['options']['elementDetailsCollapsed']} -
- {!$method|shortDescription:true} -
- {/if} - -
- {!$method|longDescription} - - {if !$class->deprecated && $method->deprecated} -

Deprecated

- {ifset $annotations['deprecated']} -
- {foreach $annotations['deprecated'] as $description} - {if $description} - {!$description|annotation:'deprecated':$method}
- {/if} - {/foreach} -
- {/ifset} - {/if} - - {if $method->parameters && isset($annotations['param'])} -

Parameters

-
- {foreach $method->parameters as $parameter} -
${$parameter->name}{if $parameter->unlimited},…{/if}
-
{ifset $annotations['param'][$parameter->position]}{!$annotations['param'][$parameter->position]|annotation:'param':$method}{/ifset}
- {/foreach} -
- {/if} - - {if isset($annotations['return']) && 'void' !== $annotations['return'][0]} -

Returns

-
- {foreach $annotations['return'] as $description} - {!$description|annotation:'return':$method}
- {/foreach} -
- {/if} - - {ifset $annotations['throws']} -

Throws

-
- {foreach $annotations['throws'] as $description} - {!$description|annotation:'throws':$method}
- {/foreach} -
- {/ifset} - - {foreach $template->annotationSort($template->annotationFilter($annotations, array('deprecated', 'param', 'return', 'throws'))) as $annotation => $descriptions} -

{$annotation|annotationBeautify}

-
- {foreach $descriptions as $description} - {if $description} - {!$description|annotation:$annotation:$method}
- {/if} - {/foreach} -
- {/foreach} - - {var $overriddenMethod = $method->overriddenMethod} - {if $overriddenMethod} -

Overrides

- - {/if} - - {var $implementedMethod = $method->implementedMethod} - {if $implementedMethod} -

Implementation of

- - {/if} -
-
- - {/define} - -

Methods summary

- - {foreach $ownMethods as $method} - {include #method, method => $method} - {/foreach} -
- - {foreach $inheritedMethods as $parentName => $methods} -

Methods inherited from {$parentName}

-

- {foreach $methods as $method} - {$method->name}(){sep}, {/sep} - {/foreach} -

- {/foreach} - - {foreach $usedMethods as $traitName => $methods} -

Methods used from {$traitName}

-

- {foreach $methods as $data} - {$data['method']->name}(){if $data['aliases']}(as {foreach $data['aliases'] as $alias}{$alias->name}(){sep}, {/sep}{/foreach}){/if}{sep}, {/sep} - {/foreach} -

- {/foreach} - -

Magic methods summary

- - {foreach $ownMagicMethods as $method} - {include #method, method => $method} - {/foreach} -
- - {foreach $inheritedMagicMethods as $parentName => $methods} -

Magic methods inherited from {$parentName}

-

- {foreach $methods as $method} - {$method->name}(){sep}, {/sep} - {/foreach} -

- {/foreach} - - {foreach $usedMagicMethods as $traitName => $methods} -

Magic methods used from {$traitName}

-

- {foreach $methods as $data} - {$data['method']->name}(){if $data['aliases']}(as {foreach $data['aliases'] as $alias}{$alias->name}(){sep}, {/sep}{/foreach}){/if}{sep}, {/sep} - {/foreach} -

- {/foreach} - {/if} - - - {var $ownConstants = $class->ownConstants} - {var $inheritedConstants = $class->inheritedConstants} - - {if $ownConstants || $inheritedConstants} -

Constants summary

- - - {var $annotations = $constant->annotations} - - - - - - -
{!$constant->typeHint|typeLinks:$constant} - {if $class->internal} - {$constant->name} - {else} - {$constant->name} - {/if} - {!$constant->valueDefinition|highlightValue:$class}
- # - - {if $config->template['options']['elementDetailsCollapsed']} -
- {!$constant|shortDescription:true} -
- {/if} - -
- {!$constant|longDescription} - - {foreach $template->annotationSort($template->annotationFilter($annotations, array('var'))) as $annotation => $descriptions} -

{$annotation|annotationBeautify}

-
- {foreach $descriptions as $description} - {if $description} - {!$description|annotation:$annotation:$constant}
- {/if} - {/foreach} -
- {/foreach} -
-
- - {foreach $inheritedConstants as $parentName => $constants} -

Constants inherited from {$parentName}

-

- {foreach $constants as $constant} - {$constant->name}{sep}, {/sep} - {/foreach} -

- {/foreach} - {/if} - - {var $ownProperties = $class->ownProperties} - {var $inheritedProperties = $class->inheritedProperties} - {var $usedProperties = $class->usedProperties} - {var $ownMagicProperties = $class->ownMagicProperties} - {var $inheritedMagicProperties = $class->inheritedMagicProperties} - {var $usedMagicProperties = $class->usedMagicProperties} - - {if $ownProperties || $inheritedProperties || $usedProperties || $ownMagicProperties || $inheritedMagicProperties || $usedMagicProperties} - {define #property} - - - {if $property->protected}protected{elseif $property->private}private{else}public{/if} {if $property->static}static{/if} {if $property->readOnly}read-only{elseif $property->writeOnly}write-only{/if} - {!$property->typeHint|typeLinks:$property} - - - - {if $class->internal} - ${$property->name} - {else} - ${$property->name} - {/if} - - {!$property->defaultValueDefinition|highlightValue:$class} -
- # - - {if $config->template['options']['elementDetailsCollapsed']} -
- {!$property|shortDescription:true} -
- {/if} - -
- {!$property|longDescription} - - {foreach $template->annotationSort($template->annotationFilter($property->annotations, array('var'))) as $annotation => $descriptions} -

{$annotation|annotationBeautify}

-
- {foreach $descriptions as $description} - {if $description} - {!$description|annotation:$annotation:$property}
- {/if} - {/foreach} -
- {/foreach} -
-
- - {/define} - -

Properties summary

- - {foreach $ownProperties as $property} - {include #property, property => $property} - {/foreach} -
- - {foreach $inheritedProperties as $parentName => $properties} -

Properties inherited from {$parentName}

-

- {foreach $properties as $property} - ${$property->name}{sep}, {/sep} - {/foreach} -

- {/foreach} - - {foreach $usedProperties as $traitName => $properties} -

Properties used from {$traitName}

-

- {foreach $properties as $property} - ${$property->name}{sep}, {/sep} - {/foreach} -

- {/foreach} - - {if $ownMagicProperties} -

Magic properties

- - {foreach $ownMagicProperties as $property} - {include #property, property => $property} - {/foreach} -
- {/if} - - {foreach $inheritedMagicProperties as $parentName => $properties} -

Magic properties inherited from {$parentName}

-

- {foreach $properties as $property} - ${$property->name}{sep}, {/sep} - {/foreach} -

- {/foreach} - - {foreach $usedMagicProperties as $traitName => $properties} -

Magic properties used from {$traitName}

-

- {foreach $properties as $property} - ${$property->name}{sep}, {/sep} - {/foreach} -

- {/foreach} - {/if} - - {else} -
-

- Documentation of this class could not be generated. -

-

- Class was originally declared in {$class->fileName|relativePath} and is invalid because of: -

-
    -
  • Class was redeclared in {$reason->getSender()->getFileName()|relativePath}.
  • -
-
- {/if} -
-{/block} diff --git a/apigen/templates/woodocs/combined.js.latte b/apigen/templates/woodocs/combined.js.latte deleted file mode 100644 index ad9fa3fb21d..00000000000 --- a/apigen/templates/woodocs/combined.js.latte +++ /dev/null @@ -1,22 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{contentType javascript} - -var ApiGen = ApiGen || {}; -ApiGen.config = {$config->template}; - -{var $scripts = ['jquery.min.js', 'jquery.cookie.js', 'jquery.sprintf.js', 'jquery.autocomplete.js', 'jquery.sortElements.js', 'main.js']} -{var $dir = dirname($template->getFile())} - -{foreach $scripts as $script} -{!file_get_contents("$dir/js/$script")} -{/foreach} diff --git a/apigen/templates/woodocs/config.neon b/apigen/templates/woodocs/config.neon deleted file mode 100644 index 6ddc2f6951a..00000000000 --- a/apigen/templates/woodocs/config.neon +++ /dev/null @@ -1,56 +0,0 @@ -require: - min: 2.8.0 - -resources: - resources: resources - -templates: - common: - overview.latte: index.html - combined.js.latte: resources/combined.js - elementlist.js.latte: elementlist.js - 404.latte: 404.html - - main: - package: - filename: package-%s.html - template: package.latte - namespace: - filename: namespace-%s.html - template: namespace.latte - class: - filename: class-%s.html - template: class.latte - constant: - filename: constant-%s.html - template: constant.latte - function: - filename: function-%s.html - template: function.latte - source: - filename: source-%s.html - template: source.latte - tree: - filename: tree.html - template: tree.latte - deprecated: - filename: deprecated.html - template: deprecated.latte - todo: - filename: todo.html - template: todo.latte - - optional: - sitemap: - filename: sitemap.xml - template: sitemap.xml.latte - opensearch: - filename: opensearch.xml - template: opensearch.xml.latte - robots: - filename: robots.txt - template: robots.txt.latte - -options: - elementDetailsCollapsed: Yes - elementsOrder: natural # alphabetical diff --git a/apigen/templates/woodocs/constant.latte b/apigen/templates/woodocs/constant.latte deleted file mode 100644 index 43ad423962f..00000000000 --- a/apigen/templates/woodocs/constant.latte +++ /dev/null @@ -1,67 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'constant'} - -{block #title}{if $constant->deprecated}Deprecated {/if}Constant {$constant->name}{/block} - -{block #content} -
-

Constant {$constant->shortName}

- - {if $constant->valid} - -
- {!$constant|longDescription} -
- -
- {if $constant->inNamespace()}Namespace: {!$constant->namespaceName|namespaceLinks}
{/if} - {if $constant->inPackage()}Package: {!$constant->packageName|packageLinks}
{/if} - {foreach $template->annotationSort($template->annotationFilter($constant->annotations, array('var'))) as $annotation => $values} - {foreach $values as $value} - {$annotation|annotationBeautify}{if $value}:{/if} - {!$value|annotation:$annotation:$constant}
- {/foreach} - {/foreach} - Located at {$constant->fileName|relativePath}
-
- - {var $annotations = $constant->annotations} - -

Value summary

- - - - - - -
{!$constant->typeHint|typeLinks:$constant}{block|strip} - {var $element = $template->resolveElement($constant->valueDefinition, $constant)} - {if $element}{$constant->valueDefinition}{else}{!$constant->valueDefinition|highlightValue:$constant}{/if} - {/block}{ifset $annotations['var']}{!$annotations['var'][0]|description:$constant}{/ifset}
- - {else} -
-

- Documentation of this constant could not be generated. -

-

- Constant was originally declared in {$constant->fileName|relativePath} and is invalid because of: -

-
    -
  • Constant was redeclared in {$reason->getSender()->getFileName()|relativePath}.
  • -
-
- {/if} -
-{/block} diff --git a/apigen/templates/woodocs/deprecated.latte b/apigen/templates/woodocs/deprecated.latte deleted file mode 100644 index 427afc7c5b6..00000000000 --- a/apigen/templates/woodocs/deprecated.latte +++ /dev/null @@ -1,137 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'deprecated'} - -{block #title}Deprecated{/block} - -{block #content} -
-

{include #title}

- - {define #classes} - - {$class->name} - - {foreach $class->annotations['deprecated'] as $description} - {if $description} - {!$description|annotation:'deprecated':$class}
- {/if} - {/foreach} - - - {/define} - - {if $deprecatedClasses} -

Classes summary

- - {include #classes, items => $deprecatedClasses} -
- {/if} - - {if $deprecatedInterfaces} -

Interfaces summary

- - {include #classes, items => $deprecatedInterfaces} -
- {/if} - - {if $deprecatedTraits} -

Traits summary

- - {include #classes, items => $deprecatedTraits} -
- {/if} - - {if $deprecatedExceptions} -

Exceptions summary

- - {include #classes, items => $deprecatedExceptions} -
- {/if} - - {if $deprecatedMethods} -

Methods summary

- - - - - - -
{$method->declaringClassName}{$method->name}() - {if $method->hasAnnotation('deprecated')} - {foreach $method->annotations['deprecated'] as $description} - {if $description} - {!$description|annotation:'deprecated':$method}
- {/if} - {/foreach} - {/if} -
- {/if} - - {if $deprecatedConstants} -

Constants summary

- - - {if $constant->declaringClassName} - - - {else} - - - {/if} - - -
{$constant->declaringClassName}{$constant->name}{$constant->namespaceName}{$constant->shortName} - {foreach $constant->annotations['deprecated'] as $description} - {if $description} - {!$description|annotation:'deprecated':$constant}
- {/if} - {/foreach} -
- {/if} - - {if $deprecatedProperties} -

Properties summary

- - - - - - -
{$property->declaringClassName}${$property->name} - {foreach $property->annotations['deprecated'] as $description} - {if $description} - {!$description|annotation:'deprecated':$property}
- {/if} - {/foreach} -
- {/if} - - {if $deprecatedFunctions} -

Functions summary

- - - - - - -
{$function->namespaceName}{$function->shortName} - {foreach $function->annotations['deprecated'] as $description} - {if $description} - {!$description|annotation:'deprecated':$function}
- {/if} - {/foreach} -
- {/if} -
-{/block} diff --git a/apigen/templates/woodocs/elementlist.js.latte b/apigen/templates/woodocs/elementlist.js.latte deleted file mode 100644 index 176b164dfd4..00000000000 --- a/apigen/templates/woodocs/elementlist.js.latte +++ /dev/null @@ -1,15 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{contentType javascript} - -var ApiGen = ApiGen || {}; -ApiGen.elements = {$elements}; diff --git a/apigen/templates/woodocs/function.latte b/apigen/templates/woodocs/function.latte deleted file mode 100644 index fba90ca4eb1..00000000000 --- a/apigen/templates/woodocs/function.latte +++ /dev/null @@ -1,98 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'function'} - -{block #title}{if $function->deprecated}Deprecated {/if}Function {$function->name}{/block} - -{block #content} -
-

Function {$function->shortName}

- - {if $function->valid} - -
- {!$function|longDescription} -
- -
- {if $function->inNamespace()}Namespace: {!$function->namespaceName|namespaceLinks}
{/if} - {if $function->inPackage()}Package: {!$function->packageName|packageLinks}
{/if} - {foreach $template->annotationSort($template->annotationFilter($function->annotations, array('param', 'return', 'throws'))) as $annotation => $values} - {foreach $values as $value} - {$annotation|annotationBeautify}{if $value}:{/if} - {!$value|annotation:$annotation:$function}
- {/foreach} - {/foreach} - Located at {$function->fileName|relativePath}
-
- - {var $annotations = $function->annotations} - - {if $function->numberOfParameters} -

Parameters summary

- - - - - - -
{!$parameter->typeHint|typeLinks:$function}{block|strip} - {if $parameter->passedByReference}& {/if}${$parameter->name}{if $parameter->defaultValueAvailable} = {!$parameter->defaultValueDefinition|highlightPHP:$function}{elseif $parameter->unlimited},…{/if} - {/block} - {ifset $annotations['param'][$parameter->position]}{!$annotations['param'][$parameter->position]|description:$parameter}{/ifset} -
- {/if} - - {if isset($annotations['return']) && 'void' !== $annotations['return'][0]} -

Return value summary

- - - - - -
- {!$annotations['return'][0]|typeLinks:$function} - - {!$annotations['return'][0]|description:$function} -
- {/if} - - {if isset($annotations['throws'])} -

Thrown exceptions summary

- - - - - -
- {!$throws|typeLinks:$function} - - {!$throws|description:$function} -
- {/if} - - {else} -
-

- Documentation of this function could not be generated. -

-

- Function was originally declared in {$function->fileName|relativePath} and is invalid because of: -

-
    -
  • Function was redeclared in {$reason->getSender()->getFileName()|relativePath}.
  • -
-
- {/if} -
-{/block} diff --git a/apigen/templates/woodocs/js/jquery.autocomplete.js b/apigen/templates/woodocs/js/jquery.autocomplete.js deleted file mode 100644 index b8bec34df5a..00000000000 --- a/apigen/templates/woodocs/js/jquery.autocomplete.js +++ /dev/null @@ -1,799 +0,0 @@ -/*! - * jQuery Autocomplete plugin 1.1 - * - * Copyright (c) 2009 Jörn Zaefferer - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $ - */ - -;(function($) { - -$.fn.extend({ - autocomplete: function(urlOrData, options) { - var isUrl = typeof urlOrData == "string"; - options = $.extend({}, $.Autocompleter.defaults, { - url: isUrl ? urlOrData : null, - data: isUrl ? null : urlOrData, - delay: isUrl ? $.Autocompleter.defaults.delay : 10, - max: options && !options.scroll ? 10 : 150 - }, options); - - // if highlight is set to false, replace it with a do-nothing function - options.highlight = options.highlight || function(value) { return value; }; - - // if the formatMatch option is not specified, then use formatItem for backwards compatibility - options.formatMatch = options.formatMatch || options.formatItem; - - options.show = options.show || function(list) {}; - - return this.each(function() { - new $.Autocompleter(this, options); - }); - }, - result: function(handler) { - return this.bind("result", handler); - }, - search: function(handler) { - return this.trigger("search", [handler]); - }, - flushCache: function() { - return this.trigger("flushCache"); - }, - setOptions: function(options){ - return this.trigger("setOptions", [options]); - }, - unautocomplete: function() { - return this.trigger("unautocomplete"); - } -}); - -$.Autocompleter = function(input, options) { - - var KEY = { - UP: 38, - DOWN: 40, - DEL: 46, - TAB: 9, - RETURN: 13, - ESC: 27, - COMMA: 188, - PAGEUP: 33, - PAGEDOWN: 34, - BACKSPACE: 8 - }; - - // Create $ object for input element - var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); - - var timeout; - var previousValue = ""; - var cache = $.Autocompleter.Cache(options); - var hasFocus = 0; - var lastKeyPressCode; - var config = { - mouseDownOnSelect: false - }; - var select = $.Autocompleter.Select(options, input, selectCurrent, config); - - // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all - $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) { - // a keypress means the input has focus - // avoids issue where input had focus before the autocomplete was applied - hasFocus = 1; - // track last key pressed - lastKeyPressCode = event.keyCode; - switch(event.keyCode) { - - case KEY.UP: - event.preventDefault(); - if ( select.visible() ) { - select.prev(); - } else { - onChange(0, true); - } - break; - - case KEY.DOWN: - event.preventDefault(); - if ( select.visible() ) { - select.next(); - } else { - onChange(0, true); - } - break; - - case KEY.PAGEUP: - event.preventDefault(); - if ( select.visible() ) { - select.pageUp(); - } else { - onChange(0, true); - } - break; - - case KEY.PAGEDOWN: - event.preventDefault(); - if ( select.visible() ) { - select.pageDown(); - } else { - onChange(0, true); - } - break; - - // matches also semicolon - case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: - case KEY.TAB: - case KEY.RETURN: - if( selectCurrent() ) { - //event.preventDefault(); - //return false; - } - break; - - case KEY.ESC: - select.hide(); - break; - - default: - clearTimeout(timeout); - timeout = setTimeout(onChange, options.delay); - break; - } - }).focus(function(){ - // track whether the field has focus, we shouldn't process any - // results if the field no longer has focus - hasFocus++; - }).blur(function() { - hasFocus = 0; - if (!config.mouseDownOnSelect) { - hideResults(); - } - }).click(function() { - // show select when clicking in a focused field - if ( hasFocus++ > 1 && !select.visible() ) { - onChange(0, true); - } - }).bind("search", function() { - // TODO why not just specifying both arguments? - var fn = (arguments.length > 1) ? arguments[1] : null; - function findValueCallback(q, data) { - var result; - if( data && data.length ) { - for (var i=0; i < data.length; i++) { - if( data[i].result.toLowerCase() == q.toLowerCase() ) { - result = data[i]; - break; - } - } - } - if( typeof fn == "function" ) fn(result); - else $input.trigger("result", result && [result.data, result.value]); - } - $.each(trimWords($input.val()), function(i, value) { - request(value, findValueCallback, findValueCallback); - }); - }).bind("flushCache", function() { - cache.flush(); - }).bind("setOptions", function() { - $.extend(options, arguments[1]); - // if we've updated the data, repopulate - if ( "data" in arguments[1] ) - cache.populate(); - }).bind("unautocomplete", function() { - select.unbind(); - $input.unbind(); - $(input.form).unbind(".autocomplete"); - }); - - - function selectCurrent() { - var selected = select.selected(); - if( !selected ) - return false; - - var v = selected.result; - previousValue = v; - - if ( options.multiple ) { - var words = trimWords($input.val()); - if ( words.length > 1 ) { - var seperator = options.multipleSeparator.length; - var cursorAt = $(input).selection().start; - var wordAt, progress = 0; - $.each(words, function(i, word) { - progress += word.length; - if (cursorAt <= progress) { - wordAt = i; - return false; - } - progress += seperator; - }); - words[wordAt] = v; - // TODO this should set the cursor to the right position, but it gets overriden somewhere - //$.Autocompleter.Selection(input, progress + seperator, progress + seperator); - v = words.join( options.multipleSeparator ); - } - v += options.multipleSeparator; - } - - $input.val(v); - hideResultsNow(); - $input.trigger("result", [selected.data, selected.value]); - return true; - } - - function onChange(crap, skipPrevCheck) { - if( lastKeyPressCode == KEY.DEL ) { - select.hide(); - return; - } - - var currentValue = $input.val(); - - if ( !skipPrevCheck && currentValue == previousValue ) - return; - - previousValue = currentValue; - - currentValue = lastWord(currentValue); - if ( currentValue.length >= options.minChars) { - $input.addClass(options.loadingClass); - if (!options.matchCase) - currentValue = currentValue.toLowerCase(); - request(currentValue, receiveData, hideResultsNow); - } else { - stopLoading(); - select.hide(); - } - }; - - function trimWords(value) { - if (!value) - return [""]; - if (!options.multiple) - return [$.trim(value)]; - return $.map(value.split(options.multipleSeparator), function(word) { - return $.trim(value).length ? $.trim(word) : null; - }); - } - - function lastWord(value) { - if ( !options.multiple ) - return value; - var words = trimWords(value); - if (words.length == 1) - return words[0]; - var cursorAt = $(input).selection().start; - if (cursorAt == value.length) { - words = trimWords(value) - } else { - words = trimWords(value.replace(value.substring(cursorAt), "")); - } - return words[words.length - 1]; - } - - // fills in the input box w/the first match (assumed to be the best match) - // q: the term entered - // sValue: the first matching result - function autoFill(q, sValue){ - // autofill in the complete box w/the first match as long as the user hasn't entered in more data - // if the last user key pressed was backspace, don't autofill - if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) { - // fill in the value (keep the case the user has typed) - $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); - // select the portion of the value not typed by the user (so the next character will erase) - $(input).selection(previousValue.length, previousValue.length + sValue.length); - } - }; - - function hideResults() { - clearTimeout(timeout); - timeout = setTimeout(hideResultsNow, 200); - }; - - function hideResultsNow() { - var wasVisible = select.visible(); - select.hide(); - clearTimeout(timeout); - stopLoading(); - if (options.mustMatch) { - // call search and run callback - $input.search( - function (result){ - // if no value found, clear the input box - if( !result ) { - if (options.multiple) { - var words = trimWords($input.val()).slice(0, -1); - $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") ); - } - else { - $input.val( "" ); - $input.trigger("result", null); - } - } - } - ); - } - }; - - function receiveData(q, data) { - if ( data && data.length && hasFocus ) { - stopLoading(); - select.display(data, q); - autoFill(q, data[0].value); - select.show(); - } else { - hideResultsNow(); - } - }; - - function request(term, success, failure) { - if (!options.matchCase) - term = term.toLowerCase(); - var data = cache.load(term); - // recieve the cached data - if (data && data.length) { - success(term, data); - // if an AJAX url has been supplied, try loading the data now - } else if( (typeof options.url == "string") && (options.url.length > 0) ){ - - var extraParams = { - timestamp: +new Date() - }; - $.each(options.extraParams, function(key, param) { - extraParams[key] = typeof param == "function" ? param() : param; - }); - - $.ajax({ - // try to leverage ajaxQueue plugin to abort previous requests - mode: "abort", - // limit abortion to this input - port: "autocomplete" + input.name, - dataType: options.dataType, - url: options.url, - data: $.extend({ - q: lastWord(term), - limit: options.max - }, extraParams), - success: function(data) { - var parsed = options.parse && options.parse(data) || parse(data); - cache.add(term, parsed); - success(term, parsed); - } - }); - } else { - // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match - select.emptyList(); - failure(term); - } - }; - - function parse(data) { - var parsed = []; - var rows = data.split("\n"); - for (var i=0; i < rows.length; i++) { - var row = $.trim(rows[i]); - if (row) { - row = row.split("|"); - parsed[parsed.length] = { - data: row, - value: row[0], - result: options.formatResult && options.formatResult(row, row[0]) || row[0] - }; - } - } - return parsed; - }; - - function stopLoading() { - $input.removeClass(options.loadingClass); - }; - -}; - -$.Autocompleter.defaults = { - inputClass: "ac_input", - resultsClass: "ac_results", - loadingClass: "ac_loading", - minChars: 1, - delay: 400, - matchCase: false, - matchSubset: true, - matchContains: false, - cacheLength: 10, - max: 100, - mustMatch: false, - extraParams: {}, - selectFirst: true, - formatItem: function(row) { return row[0]; }, - formatMatch: null, - autoFill: false, - width: 0, - multiple: false, - multipleSeparator: ", ", - highlight: function(value, term) { - return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); - }, - scroll: true, - scrollHeight: 180 -}; - -$.Autocompleter.Cache = function(options) { - - var data = {}; - var length = 0; - - function matchSubset(s, sub) { - if (!options.matchCase) - s = s.toLowerCase(); - var i = s.indexOf(sub); - if (options.matchContains == "word"){ - i = s.toLowerCase().search("\\b" + sub.toLowerCase()); - } - if (i == -1) return false; - return i == 0 || options.matchContains; - }; - - function add(q, value) { - if (length > options.cacheLength){ - flush(); - } - if (!data[q]){ - length++; - } - data[q] = value; - } - - function populate(){ - if( !options.data ) return false; - // track the matches - var stMatchSets = {}, - nullData = 0; - - // no url was specified, we need to adjust the cache length to make sure it fits the local data store - if( !options.url ) options.cacheLength = 1; - - // track all options for minChars = 0 - stMatchSets[""] = []; - - // loop through the array and create a lookup structure - for ( var i = 0, ol = options.data.length; i < ol; i++ ) { - var rawValue = options.data[i]; - // if rawValue is a string, make an array otherwise just reference the array - rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; - - var value = options.formatMatch(rawValue, i+1, options.data.length); - if ( value === false ) - continue; - - var firstChar = value.charAt(0).toLowerCase(); - // if no lookup array for this character exists, look it up now - if( !stMatchSets[firstChar] ) - stMatchSets[firstChar] = []; - - // if the match is a string - var row = { - value: value, - data: rawValue, - result: options.formatResult && options.formatResult(rawValue) || value - }; - - // push the current match into the set list - stMatchSets[firstChar].push(row); - - // keep track of minChars zero items - if ( nullData++ < options.max ) { - stMatchSets[""].push(row); - } - }; - - // add the data items to the cache - $.each(stMatchSets, function(i, value) { - // increase the cache size - options.cacheLength++; - // add to the cache - add(i, value); - }); - } - - // populate any existing data - setTimeout(populate, 25); - - function flush(){ - data = {}; - length = 0; - } - - return { - flush: flush, - add: add, - populate: populate, - load: function(q) { - if (!options.cacheLength || !length) - return null; - /* - * if dealing w/local data and matchContains than we must make sure - * to loop through all the data collections looking for matches - */ - if( !options.url && options.matchContains ){ - // track all matches - var csub = []; - // loop through all the data grids for matches - for( var k in data ){ - // don't search through the stMatchSets[""] (minChars: 0) cache - // this prevents duplicates - if( k.length > 0 ){ - var c = data[k]; - $.each(c, function(i, x) { - // if we've got a match, add it to the array - if (matchSubset(x.value, q)) { - csub.push(x); - } - }); - } - } - return csub; - } else - // if the exact item exists, use it - if (data[q]){ - return data[q]; - } else - if (options.matchSubset) { - for (var i = q.length - 1; i >= options.minChars; i--) { - var c = data[q.substr(0, i)]; - if (c) { - var csub = []; - $.each(c, function(i, x) { - if (matchSubset(x.value, q)) { - csub[csub.length] = x; - } - }); - return csub; - } - } - } - return null; - } - }; -}; - -$.Autocompleter.Select = function (options, input, select, config) { - var CLASSES = { - ACTIVE: "ac_over" - }; - - var listItems, - active = -1, - data, - term = "", - needsInit = true, - element, - list; - - // Create results - function init() { - if (!needsInit) - return; - element = $("
") - .hide() - .addClass(options.resultsClass) - .css("position", "absolute") - .appendTo(document.body); - - list = $("
    ").appendTo(element).mouseover( function(event) { - if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { - active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); - $(target(event)).addClass(CLASSES.ACTIVE); - } - }).click(function(event) { - $(target(event)).addClass(CLASSES.ACTIVE); - select(); - // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus - input.focus(); - return false; - }).mousedown(function() { - config.mouseDownOnSelect = true; - }).mouseup(function() { - config.mouseDownOnSelect = false; - }); - - if( options.width > 0 ) - element.css("width", options.width); - - needsInit = false; - } - - function target(event) { - var element = event.target; - while(element && element.tagName != "LI") - element = element.parentNode; - // more fun with IE, sometimes event.target is empty, just ignore it then - if(!element) - return []; - return element; - } - - function moveSelect(step) { - listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); - movePosition(step); - var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); - if(options.scroll) { - var offset = 0; - listItems.slice(0, active).each(function() { - offset += this.offsetHeight; - }); - if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { - list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight()); - } else if(offset < list.scrollTop()) { - list.scrollTop(offset); - } - } - }; - - function movePosition(step) { - active += step; - if (active < 0) { - active = listItems.size() - 1; - } else if (active >= listItems.size()) { - active = 0; - } - } - - function limitNumberOfItems(available) { - return options.max && options.max < available - ? options.max - : available; - } - - function fillList() { - list.empty(); - var max = limitNumberOfItems(data.length); - for (var i=0; i < max; i++) { - if (!data[i]) - continue; - var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term); - if ( formatted === false ) - continue; - var li = $("
  • ").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0]; - $.data(li, "ac_data", data[i]); - } - listItems = list.find("li"); - if ( options.selectFirst ) { - listItems.slice(0, 1).addClass(CLASSES.ACTIVE); - active = 0; - } - // apply bgiframe if available - if ( $.fn.bgiframe ) - list.bgiframe(); - } - - return { - display: function(d, q) { - init(); - data = d; - term = q; - fillList(); - }, - next: function() { - moveSelect(1); - }, - prev: function() { - moveSelect(-1); - }, - pageUp: function() { - if (active != 0 && active - 8 < 0) { - moveSelect( -active ); - } else { - moveSelect(-8); - } - }, - pageDown: function() { - if (active != listItems.size() - 1 && active + 8 > listItems.size()) { - moveSelect( listItems.size() - 1 - active ); - } else { - moveSelect(8); - } - }, - hide: function() { - element && element.hide(); - listItems && listItems.removeClass(CLASSES.ACTIVE); - active = -1; - }, - visible : function() { - return element && element.is(":visible"); - }, - current: function() { - return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); - }, - show: function() { - var offset = $(input).offset(); - element.css({ - width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).innerWidth(), - top: offset.top + input.offsetHeight, - left: offset.left - }).show(); - options.show(element); - if(options.scroll) { - list.scrollTop(0); - list.css({ - maxHeight: options.scrollHeight, - overflow: 'auto' - }); - - if($.browser.msie && typeof document.body.style.maxHeight === "undefined") { - var listHeight = 0; - listItems.each(function() { - listHeight += this.offsetHeight; - }); - var scrollbarsVisible = listHeight > options.scrollHeight; - list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight ); - if (!scrollbarsVisible) { - // IE doesn't recalculate width when scrollbar disappears - listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) ); - } - } - - } - }, - selected: function() { - var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); - return selected && selected.length && $.data(selected[0], "ac_data"); - }, - emptyList: function (){ - list && list.empty(); - }, - unbind: function() { - element && element.remove(); - } - }; -}; - -$.fn.selection = function(start, end) { - if (start !== undefined) { - return this.each(function() { - if( this.createTextRange ){ - var selRange = this.createTextRange(); - if (end === undefined || start == end) { - selRange.move("character", start); - selRange.select(); - } else { - selRange.collapse(true); - selRange.moveStart("character", start); - selRange.moveEnd("character", end); - selRange.select(); - } - } else if( this.setSelectionRange ){ - this.setSelectionRange(start, end); - } else if( this.selectionStart ){ - this.selectionStart = start; - this.selectionEnd = end; - } - }); - } - var field = this[0]; - if ( field.createTextRange ) { - var range = document.selection.createRange(), - orig = field.value, - teststring = "<->", - textLength = range.text.length; - range.text = teststring; - var caretAt = field.value.indexOf(teststring); - field.value = orig; - this.selection(caretAt, caretAt + textLength); - return { - start: caretAt, - end: caretAt + textLength - } - } else if( field.selectionStart !== undefined ){ - return { - start: field.selectionStart, - end: field.selectionEnd - } - } -}; - -})(jQuery); \ No newline at end of file diff --git a/apigen/templates/woodocs/js/jquery.cookie.js b/apigen/templates/woodocs/js/jquery.cookie.js deleted file mode 100644 index 6df1faca25f..00000000000 --- a/apigen/templates/woodocs/js/jquery.cookie.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Cookie plugin - * - * Copyright (c) 2006 Klaus Hartl (stilbuero.de) - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - */ - -/** - * Create a cookie with the given name and value and other optional parameters. - * - * @example $.cookie('the_cookie', 'the_value'); - * @desc Set the value of a cookie. - * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true }); - * @desc Create a cookie with all available options. - * @example $.cookie('the_cookie', 'the_value'); - * @desc Create a session cookie. - * @example $.cookie('the_cookie', null); - * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain - * used when the cookie was set. - * - * @param String name The name of the cookie. - * @param String value The value of the cookie. - * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. - * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. - * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. - * If set to null or omitted, the cookie will be a session cookie and will not be retained - * when the the browser exits. - * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). - * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). - * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will - * require a secure protocol (like HTTPS). - * @type undefined - * - * @name $.cookie - * @cat Plugins/Cookie - * @author Klaus Hartl/klaus.hartl@stilbuero.de - */ - -/** - * Get the value of a cookie with the given name. - * - * @example $.cookie('the_cookie'); - * @desc Get the value of a cookie. - * - * @param String name The name of the cookie. - * @return The value of the cookie. - * @type String - * - * @name $.cookie - * @cat Plugins/Cookie - * @author Klaus Hartl/klaus.hartl@stilbuero.de - */ -jQuery.cookie = function(name, value, options) { - if (typeof value != 'undefined') { // name and value given, set cookie - options = options || {}; - if (value === null) { - value = ''; - options.expires = -1; - } - var expires = ''; - if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { - var date; - if (typeof options.expires == 'number') { - date = new Date(); - date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); - } else { - date = options.expires; - } - expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE - } - // CAUTION: Needed to parenthesize options.path and options.domain - // in the following expressions, otherwise they evaluate to undefined - // in the packed version for some reason... - var path = options.path ? '; path=' + (options.path) : ''; - var domain = options.domain ? '; domain=' + (options.domain) : ''; - var secure = options.secure ? '; secure' : ''; - document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); - } else { // only name given, get cookie - var cookieValue = null; - if (document.cookie && document.cookie != '') { - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var cookie = jQuery.trim(cookies[i]); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) == (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; - } -}; \ No newline at end of file diff --git a/apigen/templates/woodocs/js/jquery.min.js b/apigen/templates/woodocs/js/jquery.min.js deleted file mode 100644 index 3ca5e0f5dee..00000000000 --- a/apigen/templates/woodocs/js/jquery.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v1.7 jquery.com | jquery.org/license */ -(function(a,b){function cA(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cx(a){if(!cm[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cn||(cn=c.createElement("iframe"),cn.frameBorder=cn.width=cn.height=0),b.appendChild(cn);if(!co||!cn.createElement)co=(cn.contentWindow||cn.contentDocument).document,co.write((c.compatMode==="CSS1Compat"?"":"")+""),co.close();d=co.createElement(a),co.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cn)}cm[a]=e}return cm[a]}function cw(a,b){var c={};f.each(cs.concat.apply([],cs.slice(0,b)),function(){c[this]=a});return c}function cv(){ct=b}function cu(){setTimeout(cv,0);return ct=f.now()}function cl(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ck(){try{return new a.XMLHttpRequest}catch(b){}}function ce(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bB(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function br(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bi,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bq(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bp(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bp)}function bp(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bo(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bn(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bm(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(){return!0}function M(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z]|[0-9])/ig,x=/^-ms-/,y=function(a,b){return(b+"").toUpperCase()},z=d.userAgent,A,B,C,D=Object.prototype.toString,E=Object.prototype.hasOwnProperty,F=Array.prototype.push,G=Array.prototype.slice,H=String.prototype.trim,I=Array.prototype.indexOf,J={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7",length:0,size:function(){return this.length},toArray:function(){return G.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?F.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),B.add(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(G.apply(this,arguments),"slice",G.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:F,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;B.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!B){B=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",C,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",C),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&K()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return a!=null&&m.test(a)&&!isNaN(a)},type:function(a){return a==null?String(a):J[D.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!E.call(a,"constructor")&&!E.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||E.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(x,"ms-").replace(w,y)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
    a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,unknownElems:!!a.getElementsByTagName("nav").length,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",enctype:!!c.createElement("form").enctype,submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.lastChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},m&&f.extend(p,{position:"absolute",left:"-999px",top:"-999px"});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
    ",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
    t
    ",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;f(function(){var a,b,d,e,g,h,i=1,j="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",l="visibility:hidden;border:0;",n="style='"+j+"border:5px solid #000;padding:0;'",p="
    "+""+"
    ";m=c.getElementsByTagName("body")[0];!m||(a=c.createElement("div"),a.style.cssText=l+"width:0;height:0;position:static;top:0;margin-top:"+i+"px",m.insertBefore(a,m.firstChild),o=c.createElement("div"),o.style.cssText=j+l,o.innerHTML=p,a.appendChild(o),b=o.firstChild,d=b.firstChild,g=b.nextSibling.firstChild.firstChild,h={doesNotAddBorder:d.offsetTop!==5,doesAddBorderForTableAndCells:g.offsetTop===5},d.style.position="fixed",d.style.top="20px",h.fixedPosition=d.offsetTop===20||d.offsetTop===15,d.style.position=d.style.top="",b.style.overflow="hidden",b.style.position="relative",h.subtractsBorderForOverflowNotVisible=d.offsetTop===-5,h.doesNotIncludeMarginInBodyOffset=m.offsetTop!==i,m.removeChild(a),o=a=null,f.extend(k,h))}),o.innerHTML="",n.removeChild(o),o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[f.expando]:a[f.expando]&&f.expando,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[f.expando]=n=++f.uuid:n=f.expando),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[f.expando]:f.expando;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)?b=b:b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" "));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];if(!arguments.length){if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}return b}e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!a||j===3||j===8||j===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g},removeAttr:function(a,b){var c,d,e,g,h=0;if(a.nodeType===1){d=(b||"").split(p),g=d.length;for(;h=0}})});var z=/\.(.*)$/,A=/^(?:textarea|input|select)$/i,B=/\./g,C=/ /g,D=/[^\w\s.|`]/g,E=/^([^\.]*)?(?:\.(.+))?$/,F=/\bhover(\.\S+)?/,G=/^key/,H=/^(?:mouse|contextmenu)|click/,I=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,J=function(a){var b=I.exec(a);b&& -(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},K=function(a,b){return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||a.id===b[2])&&(!b[3]||b[3].test(a.className))},L=function(a){return f.event.special.hover?a:a.replace(F,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=L(c).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"",(g||!e)&&c.preventDefault();if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,n=null;for(m=e.parentNode;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l=0:t===b&&(t=o[s]=r.quick?K(m,r.quick):f(m).is(s)),t&&q.push(r);q.length&&j.push({elem:m,matches:q})}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),G.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),H.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

    ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
    ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var Y="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",Z=/ jQuery\d+="(?:\d+|null)"/g,$=/^\s+/,_=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,ba=/<([\w:]+)/,bb=/",""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bk=X(c);bj.optgroup=bj.option,bj.tbody=bj.tfoot=bj.colgroup=bj.caption=bj.thead,bj.th=bj.td,f.support.htmlSerialize||(bj._default=[1,"div
    ","
    "]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after" -,arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Z,""):null;if(typeof a=="string"&&!bd.test(a)&&(f.support.leadingWhitespace||!$.test(a))&&!bj[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(_,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bn(a,d),e=bo(a),g=bo(d);for(h=0;e[h];++h)g[h]&&bn(e[h],g[h])}if(b){bm(a,d);if(c){e=bo(a),g=bo(d);for(h=0;e[h];++h)bm(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!bc.test(k))k=b.createTextNode(k);else{k=k.replace(_,"<$1>");var l=(ba.exec(k)||["",""])[1].toLowerCase(),m=bj[l]||bj._default,n=m[0],o=b.createElement("div");b===c?bk.appendChild(o):X(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=bb.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&$.test(k)&&o.insertBefore(b.createTextNode($.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bt.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bs,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bs.test(g)?g.replace(bs,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bB(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bC=function(a,c){var d,e,g;c=c.replace(bu,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bD=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bv.test(f)&&bw.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bB=bC||bD,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bF=/%20/g,bG=/\[\]$/,bH=/\r?\n/g,bI=/#.*$/,bJ=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bK=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bL=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bM=/^(?:GET|HEAD)$/,bN=/^\/\//,bO=/\?/,bP=/)<[^<]*)*<\/script>/gi,bQ=/^(?:select|textarea)/i,bR=/\s+/,bS=/([?&])_=[^&]*/,bT=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bU=f.fn.load,bV={},bW={},bX,bY,bZ=["*/"]+["*"];try{bX=e.href}catch(b$){bX=c.createElement("a"),bX.href="",bX=bX.href}bY=bT.exec(bX.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bU)return bU.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
    ").append(c.replace(bP,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bQ.test(this.nodeName)||bK.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bH,"\r\n")}}):{name:b.name,value:c.replace(bH,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?cb(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),cb(a,b);return a},ajaxSettings:{url:bX,isLocal:bL.test(bY[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bZ},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:b_(bV),ajaxTransport:b_(bW),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cd(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=ce(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bJ.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bI,"").replace(bN,bY[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bR),d.crossDomain==null&&(r=bT.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bY[1]&&r[2]==bY[2]&&(r[3]||(r[1]==="http:"?80:443))==(bY[3]||(bY[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),ca(bV,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bM.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bO.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bS,"$1_="+x);d.url=y+(y===d.url?(bO.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bZ+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=ca(bW,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){s<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)cc(g,a[g],c,e);return d.join("&").replace(bF,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cf=f.now(),cg=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cf++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cg.test(b.url)||e&&cg.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cg,l),b.url===j&&(e&&(k=k.replace(cg,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ch=a.ActiveXObject?function(){for(var a in cj)cj[a](0,1)}:!1,ci=0,cj;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ck()||cl()}:ck,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ch&&delete cj[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++ci,ch&&(cj||(cj={},f(a).unload(ch)),cj[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cm={},cn,co,cp=/^(?:toggle|show|hide)$/,cq=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cr,cs=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],ct;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cw("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cz.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cz.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cA(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cA(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff --git a/apigen/templates/woodocs/js/main.js b/apigen/templates/woodocs/js/main.js deleted file mode 100644 index 3854e1896f0..00000000000 --- a/apigen/templates/woodocs/js/main.js +++ /dev/null @@ -1,292 +0,0 @@ -/*! - * ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - * - * Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) - * Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) - * Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) - * Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - * - * For the full copyright and license information, please view - * the file LICENSE.md that was distributed with this source code. - */ - -$(function() { - var $document = $(document); - var $navigation = $('#navigation'); - var navigationHeight = $('#navigation').height(); - var $left = $('#left'); - var $right = $('#right'); - var $rightInner = $('#rightInner'); - var $splitter = $('#splitter'); - var $groups = $('#groups'); - var $content = $('#content'); - - // Menu - - // Hide deep packages and namespaces - $('ul span', $groups).click(function(event) { - event.preventDefault(); - event.stopPropagation(); - $(this) - .toggleClass('collapsed') - .parent() - .next('ul') - .toggleClass('collapsed'); - }).click(); - - $active = $('ul li.active', $groups); - if ($active.length > 0) { - // Open active - $('> a > span', $active).click(); - } else { - $main = $('> ul > li.main', $groups); - if ($main.length > 0) { - // Open first level of the main project - $('> a > span', $main).click(); - } else { - // Open first level of all - $('> ul > li > a > span', $groups).click(); - } - } - - // Content - - // Search autocompletion - var autocompleteFound = false; - var autocompleteFiles = {'c': 'class', 'co': 'constant', 'f': 'function', 'm': 'class', 'mm': 'class', 'p': 'class', 'mp': 'class', 'cc': 'class'}; - var $search = $('#search input[name=q]'); - $search - .autocomplete(ApiGen.elements, { - matchContains: true, - scrollHeight: 200, - max: 20, - formatItem: function(data) { - return data[1].replace(/^(.+\\)(.+)$/, '$1$2'); - }, - formatMatch: function(data) { - return data[1]; - }, - formatResult: function(data) { - return data[1]; - }, - show: function($list) { - var $items = $('li span', $list); - var maxWidth = Math.max.apply(null, $items.map(function() { - return $(this).width(); - })); - // 10px padding - $list.width(Math.max(maxWidth + 10, $search.innerWidth())); - } - }).result(function(event, data) { - autocompleteFound = true; - var location = window.location.href.split('/'); - location.pop(); - var parts = data[1].split(/::|$/); - var file = $.sprintf(ApiGen.config.templates.main[autocompleteFiles[data[0]]].filename, parts[0].replace(/\(\)/, '').replace(/[^\w]/g, '.')); - if (parts[1]) { - file += '#' + ('mm' === data[0] || 'mp' === data[0] ? 'm' : '') + parts[1].replace(/([\w]+)\(\)/, '_$1'); - } - location.push(file); - window.location = location.join('/'); - - // Workaround for Opera bug - $(this).closest('form').attr('action', location.join('/')); - }).closest('form') - .submit(function() { - var query = $search.val(); - if ('' === query) { - return false; - } - - var label = $('#search input[name=more]').val(); - if (!autocompleteFound && label && -1 === query.indexOf('more:')) { - $search.val(query + ' more:' + label); - } - - return !autocompleteFound && '' !== $('#search input[name=cx]').val(); - }); - - // Save natural order - $('table.summary tr[data-order]', $content).each(function(index) { - do { - index = '0' + index; - } while (index.length < 3); - $(this).attr('data-order-natural', index); - }); - - // Switch between natural and alphabetical order - var $caption = $('table.summary', $content) - .filter(':has(tr[data-order])') - .prev('h2'); - $caption - .click(function() { - var $this = $(this); - var order = $this.data('order') || 'natural'; - order = 'natural' === order ? 'alphabetical' : 'natural'; - $this.data('order', order); - $.cookie('order', order, {expires: 365}); - var attr = 'alphabetical' === order ? 'data-order' : 'data-order-natural'; - $this - .next('table') - .find('tr').sortElements(function(a, b) { - return $(a).attr(attr) > $(b).attr(attr) ? 1 : -1; - }); - return false; - }) - .addClass('switchable') - .attr('title', 'Switch between natural and alphabetical order'); - if ((null === $.cookie('order') && 'alphabetical' === ApiGen.config.options.elementsOrder) || 'alphabetical' === $.cookie('order')) { - $caption.click(); - } - - // Open details - if (ApiGen.config.options.elementDetailsCollapsed) { - $('tr', $content).filter(':has(.detailed)') - .click(function() { - var $this = $(this); - $('.short', $this).hide(); - $('.detailed', $this).show(); - }); - } - - // Splitter - var splitterWidth = $splitter.width(); - function setSplitterPosition(position) - { - $left.width(position); - $right.css('margin-left', position + splitterWidth); - $splitter.css('left', position); - } - function setNavigationPosition() - { - var height = $(window).height() - navigationHeight; - $left.height(height); - $splitter.height(height); - $right.height(height); - } - function setContentWidth() - { - var width = $rightInner.width(); - $rightInner - .toggleClass('medium', width <= 960) - .toggleClass('small', width <= 650); - } - $splitter.mousedown(function() { - $splitter.addClass('active'); - - $document.mousemove(function(event) { - if (event.pageX >= 230 && $document.width() - event.pageX >= 600 + splitterWidth) { - setSplitterPosition(event.pageX); - setContentWidth(); - } - }); - - $() - .add($splitter) - .add($document) - .mouseup(function() { - $splitter - .removeClass('active') - .unbind('mouseup'); - $document - .unbind('mousemove') - .unbind('mouseup'); - - $.cookie('splitter', parseInt($splitter.css('left')), {expires: 365}); - }); - - return false; - }); - var splitterPosition = $.cookie('splitter'); - if (null !== splitterPosition) { - setSplitterPosition(parseInt(splitterPosition)); - } - setNavigationPosition(); - setContentWidth(); - $(window) - .resize(setNavigationPosition) - .resize(setContentWidth); - - // Select selected lines - var matches = window.location.hash.substr(1).match(/^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/); - if (null !== matches) { - var lists = matches[0].split(','); - for (var i = 0; i < lists.length; i++) { - var lines = lists[i].split('-'); - lines[1] = lines[1] || lines[0]; - for (var j = lines[0]; j <= lines[1]; j++) { - $('#' + j).addClass('selected'); - } - } - - var $firstLine = $('#' + parseInt(matches[0])); - if ($firstLine.length > 0) { - $right.scrollTop($firstLine.offset().top); - } - } - - // Save selected lines - var lastLine; - $('a.l').click(function(event) { - event.preventDefault(); - - var $selectedLine = $(this).parent(); - var selectedLine = parseInt($selectedLine.attr('id')); - - if (event.shiftKey) { - if (lastLine) { - for (var i = Math.min(selectedLine, lastLine); i <= Math.max(selectedLine, lastLine); i++) { - $('#' + i).addClass('selected'); - } - } else { - $selectedLine.addClass('selected'); - } - } else if (event.ctrlKey) { - $selectedLine.toggleClass('selected'); - } else { - var $selected = $('.l.selected') - .not($selectedLine) - .removeClass('selected'); - if ($selected.length > 0) { - $selectedLine.addClass('selected'); - } else { - $selectedLine.toggleClass('selected'); - } - } - - lastLine = $selectedLine.hasClass('selected') ? selectedLine : null; - - // Update hash - var lines = $('.l.selected') - .map(function() { - return parseInt($(this).attr('id')); - }) - .get() - .sort(function(a, b) { - return a - b; - }); - - var hash = []; - var list = []; - for (var j = 0; j < lines.length; j++) { - if (0 === j && j + 1 === lines.length) { - hash.push(lines[j]); - } else if (0 === j) { - list[0] = lines[j]; - } else if (lines[j - 1] + 1 !== lines[j] && j + 1 === lines.length) { - hash.push(list.join('-')); - hash.push(lines[j]); - } else if (lines[j - 1] + 1 !== lines[j]) { - hash.push(list.join('-')); - list = [lines[j]]; - } else if (j + 1 === lines.length) { - list[1] = lines[j]; - hash.push(list.join('-')); - } else { - list[1] = lines[j]; - } - } - - window.location.hash = hash.join(','); - }); -}); diff --git a/apigen/templates/woodocs/namespace.latte b/apigen/templates/woodocs/namespace.latte deleted file mode 100644 index 67b745560cc..00000000000 --- a/apigen/templates/woodocs/namespace.latte +++ /dev/null @@ -1,32 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'namespace'} - -{block #title}{if $namespace != 'None'}Namespace {$namespace}{else}No namespace{/if}{/block} - -{block #content} -
    -

    {if $namespace != 'None'}Namespace {!$namespace|namespaceLinks:false}{else}No namespace{/if}

    - - {if $subnamespaces} -

    Namespaces summary

    -
    - - - -
    {$namespace}
    - {/if} - - {include '@elementlist.latte'} -
-{/block} diff --git a/apigen/templates/woodocs/opensearch.xml.latte b/apigen/templates/woodocs/opensearch.xml.latte deleted file mode 100644 index ae5dfb9d0bb..00000000000 --- a/apigen/templates/woodocs/opensearch.xml.latte +++ /dev/null @@ -1,21 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} - - -{$config->title} -{$config->title} Documentation - -{$config->baseUrl}/favicon.ico -open -UTF-8 -UTF-8 - diff --git a/apigen/templates/woodocs/overview.latte b/apigen/templates/woodocs/overview.latte deleted file mode 100644 index 9e65c747856..00000000000 --- a/apigen/templates/woodocs/overview.latte +++ /dev/null @@ -1,57 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'overview'} - -{block #title}{$config->title ?: 'Overview'}{/block} - -{block #content} -
-

{include #title}

- - {var $group = false} - - {if $namespaces} - {if} -

Namespaces summary

- - {foreach $namespaces as $namespace} - {continueIf $config->main && 0 !== strpos($namespace, $config->main)} - - {var $group = true} - - - {/foreach} -
{$namespace}
- {/if $iterations} - {/if} - - {if $packages} - {if} -

Packages summary

- - {foreach $packages as $package} - {continueIf $config->main && 0 !== strpos($package, $config->main)} - - {var $group = true} - - - {/foreach} -
{$package}
- {/if $iterations} - {/if} - - {if !$group} - {include '@elementlist.latte'} - {/if} -
-{/block} \ No newline at end of file diff --git a/apigen/templates/woodocs/package.latte b/apigen/templates/woodocs/package.latte deleted file mode 100644 index f0f31c78782..00000000000 --- a/apigen/templates/woodocs/package.latte +++ /dev/null @@ -1,32 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'package'} - -{block #title}{if $package != 'None'}Package {$package}{else}No package{/if}{/block} - -{block #content} -
-

{if $package != 'None'}Package {!$package|packageLinks:false}{else}No package{/if}

- - {if $subpackages} -

Packages summary

- - - - -
{$package}
- {/if} - - {include '@elementlist.latte'} -
-{/block} diff --git a/apigen/templates/woodocs/resources/bootstrap.min.css b/apigen/templates/woodocs/resources/bootstrap.min.css deleted file mode 100644 index c98662bc54e..00000000000 --- a/apigen/templates/woodocs/resources/bootstrap.min.css +++ /dev/null @@ -1,632 +0,0 @@ -article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;} -audio,canvas,video{display:inline-block;*display:inline;*zoom:1;} -audio:not([controls]){display:none;} -html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;} -a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} -a:hover,a:active{outline:0;} -sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;} -sup{top:-0.5em;} -sub{bottom:-0.25em;} -img{max-width:100%;height:auto;border:0;-ms-interpolation-mode:bicubic;} -button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;} -button,input{*overflow:visible;line-height:normal;} -button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;} -button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;} -input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;} -input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;} -textarea{overflow:auto;vertical-align:top;} -.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";} -.clearfix:after{clear:both;} -body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;} -a{color:#985c81;text-decoration:none;} -a:hover{color:#784966;text-decoration:underline;} -.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} -.row:after{clear:both;} -[class*="span"]{float:left;margin-left:20px;} -.span1{width:60px;} -.span2{width:140px;} -.span3{width:220px;} -.span4{width:300px;} -.span5{width:380px;} -.span6{width:460px;} -.span7{width:540px;} -.span8{width:620px;} -.span9{width:700px;} -.span10{width:780px;} -.span11{width:860px;} -.span12,.container{width:940px;} -.offset1{margin-left:100px;} -.offset2{margin-left:180px;} -.offset3{margin-left:260px;} -.offset4{margin-left:340px;} -.offset5{margin-left:420px;} -.offset6{margin-left:500px;} -.offset7{margin-left:580px;} -.offset8{margin-left:660px;} -.offset9{margin-left:740px;} -.offset10{margin-left:820px;} -.offset11{margin-left:900px;} -.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} -.row-fluid:after{clear:both;} -.row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;} -.row-fluid>[class*="span"]:first-child{margin-left:0;} -.row-fluid>.span1{width:6.382978723%;} -.row-fluid>.span2{width:14.89361702%;} -.row-fluid>.span3{width:23.404255317%;} -.row-fluid>.span4{width:31.914893614%;} -.row-fluid>.span5{width:40.425531911%;} -.row-fluid>.span6{width:48.93617020799999%;} -.row-fluid>.span7{width:57.446808505%;} -.row-fluid>.span8{width:65.95744680199999%;} -.row-fluid>.span9{width:74.468085099%;} -.row-fluid>.span10{width:82.97872339599999%;} -.row-fluid>.span11{width:91.489361693%;} -.row-fluid>.span12{width:99.99999998999999%;} -.container{width:940px;margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";} -.container:after{clear:both;} -.container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";} -.container-fluid:after{clear:both;} -p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;} -.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;} -h1,h2,h3,h4,h5,h6{margin:0;font-weight:bold;color:#333333;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;} -h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;} -h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;} -h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;} -h4,h5,h6{line-height:18px;} -h4{font-size:14px;}h4 small{font-size:12px;} -h5{font-size:12px;} -h6{font-size:11px;color:#999999;text-transform:uppercase;} -.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;} -.page-header h1{line-height:1;} -ul,ol{padding:0;margin:0 0 9px 25px;} -ul ul,ul ol,ol ol,ol ul{margin-bottom:0;} -ul{list-style:disc;} -ol{list-style:decimal;} -li{line-height:18px;} -ul.unstyled,ol.unstyled{margin-left:0;list-style:none;} -dl{margin-bottom:18px;} -dt,dd{line-height:18px;} -dt{font-weight:bold;} -dd{margin-left:9px;} -hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;} -strong{font-weight:bold;} -em{font-style:italic;} -.muted{color:#999999;} -abbr{font-size:90%;text-transform:uppercase;border-bottom:1px dotted #ddd;cursor:help;} -blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;} -blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';} -blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;} -q:before,q:after,blockquote:before,blockquote:after{content:"";} -address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;} -small{font-size:100%;} -cite{font-style:normal;} -code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} -code{padding:3px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;} -pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12px;line-height:18px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;word-wrap:break-word;}pre.prettyprint{margin-bottom:18px;} -pre code{padding:0;color:inherit;background-color:transparent;border:0;} -.pre-scrollable{max-height:340px;overflow-y:scroll;} -form{margin:0 0 18px;} -fieldset{padding:0;margin:0;border:0;} -legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333333;border:0;border-bottom:1px solid #eee;}legend small{font-size:13.5px;color:#999999;} -label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px;} -input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;} -label{display:block;margin-bottom:5px;color:#333333;} -input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555555;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} -.uneditable-textarea{width:auto;height:auto;} -label input,label textarea,label select{display:block;} -input[type="image"],input[type="checkbox"],input[type="radio"]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border:0 \9;} -input[type="image"]{border:0;} -input[type="file"]{width:auto;padding:initial;line-height:initial;border:initial;background-color:#ffffff;background-color:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} -input[type="button"],input[type="reset"],input[type="submit"]{width:auto;height:auto;} -select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px;} -input[type="file"]{line-height:18px \9;} -select{width:220px;background-color:#ffffff;} -select[multiple],select[size]{height:auto;} -input[type="image"]{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} -textarea{height:auto;} -input[type="hidden"]{display:none;} -.radio,.checkbox{padding-left:18px;} -.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;} -.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;} -.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;} -.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;} -input,textarea{-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;} -input:focus,textarea:focus{border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);outline:0;outline:thin dotted \9;} -input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} -.input-mini{width:60px;} -.input-small{width:90px;} -.input-medium{width:150px;} -.input-large{width:210px;} -.input-xlarge{width:270px;} -.input-xxlarge{width:530px;} -input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{float:none;margin-left:0;} -input.span1,textarea.span1,.uneditable-input.span1{width:50px;} -input.span2,textarea.span2,.uneditable-input.span2{width:130px;} -input.span3,textarea.span3,.uneditable-input.span3{width:210px;} -input.span4,textarea.span4,.uneditable-input.span4{width:290px;} -input.span5,textarea.span5,.uneditable-input.span5{width:370px;} -input.span6,textarea.span6,.uneditable-input.span6{width:450px;} -input.span7,textarea.span7,.uneditable-input.span7{width:530px;} -input.span8,textarea.span8,.uneditable-input.span8{width:610px;} -input.span9,textarea.span9,.uneditable-input.span9{width:690px;} -input.span10,textarea.span10,.uneditable-input.span10{width:770px;} -input.span11,textarea.span11,.uneditable-input.span11{width:850px;} -input.span12,textarea.span12,.uneditable-input.span12{width:930px;} -input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#f5f5f5;border-color:#ddd;cursor:not-allowed;} -.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;} -.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853;}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e;} -.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;} -.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;} -.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48;}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392;} -.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;} -.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;} -.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847;}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b;} -.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;} -input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;} -.form-actions{padding:17px 20px 18px;margin-top:18px;margin-bottom:18px;background-color:#f5f5f5;border-top:1px solid #ddd;} -.uneditable-input{display:block;background-color:#ffffff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;} -:-moz-placeholder{color:#999999;} -::-webkit-input-placeholder{color:#999999;} -.help-block{display:block;margin-top:5px;margin-bottom:0;color:#999999;} -.help-inline{display:inline-block;*display:inline;*zoom:1;margin-bottom:9px;vertical-align:middle;padding-left:5px;} -.input-prepend,.input-append{margin-bottom:5px;*zoom:1;}.input-prepend:before,.input-append:before,.input-prepend:after,.input-append:after{display:table;content:"";} -.input-prepend:after,.input-append:after{clear:both;} -.input-prepend input,.input-append input,.input-prepend .uneditable-input,.input-append .uneditable-input{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-prepend input:focus,.input-append input:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{position:relative;z-index:2;} -.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc;} -.input-prepend .add-on,.input-append .add-on{float:left;display:block;width:auto;min-width:16px;height:18px;margin-right:-1px;padding:4px 5px;font-weight:normal;line-height:18px;color:#999999;text-align:center;text-shadow:0 1px 0 #ffffff;background-color:#f5f5f5;border:1px solid #ccc;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} -.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546;} -.input-prepend .add-on{*margin-top:1px;} -.input-append input,.input-append .uneditable-input{float:left;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} -.input-append .uneditable-input{border-left-color:#eee;border-right-color:#ccc;} -.input-append .add-on{margin-right:0;margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} -.input-append input:first-child{*margin-left:-160px;}.input-append input:first-child+.add-on{*margin-left:-21px;} -.search-query{padding-left:14px;padding-right:14px;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;} -.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input{display:inline-block;margin-bottom:0;} -.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;} -.form-search label,.form-inline label,.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{display:inline-block;} -.form-search .input-append .add-on,.form-inline .input-prepend .add-on,.form-search .input-append .add-on,.form-inline .input-prepend .add-on{vertical-align:middle;} -.form-search .radio,.form-inline .radio,.form-search .checkbox,.form-inline .checkbox{margin-bottom:0;vertical-align:middle;} -.control-group{margin-bottom:9px;} -legend+.control-group{margin-top:18px;-webkit-margin-top-collapse:separate;} -.form-horizontal .control-group{margin-bottom:18px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";} -.form-horizontal .control-group:after{clear:both;} -.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right;} -.form-horizontal .controls{margin-left:160px;} -.form-horizontal .form-actions{padding-left:160px;} -table{max-width:100%;border-collapse:collapse;border-spacing:0;} -.table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #ddd;} -.table th{font-weight:bold;} -.table thead th{vertical-align:bottom;} -.table thead:first-child tr th,.table thead:first-child tr td{border-top:0;} -.table tbody+tbody{border-top:2px solid #ddd;} -.table-condensed th,.table-condensed td{padding:4px 5px;} -.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th+th,.table-bordered td+td,.table-bordered th+td,.table-bordered td+th{border-left:1px solid #ddd;} -.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;} -.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;} -.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;} -.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;} -.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;} -.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;} -.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;} -table .span1{float:none;width:44px;margin-left:0;} -table .span2{float:none;width:124px;margin-left:0;} -table .span3{float:none;width:204px;margin-left:0;} -table .span4{float:none;width:284px;margin-left:0;} -table .span5{float:none;width:364px;margin-left:0;} -table .span6{float:none;width:444px;margin-left:0;} -table .span7{float:none;width:524px;margin-left:0;} -table .span8{float:none;width:604px;margin-left:0;} -table .span9{float:none;width:684px;margin-left:0;} -table .span10{float:none;width:764px;margin-left:0;} -table .span11{float:none;width:844px;margin-left:0;} -table .span12{float:none;width:924px;margin-left:0;} -[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;*margin-right:.3em;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;} -.icon-white{background-image:url("../img/glyphicons-halflings-white.png");} -.icon-glass{background-position:0 0;} -.icon-music{background-position:-24px 0;} -.icon-search{background-position:-48px 0;} -.icon-envelope{background-position:-72px 0;} -.icon-heart{background-position:-96px 0;} -.icon-star{background-position:-120px 0;} -.icon-star-empty{background-position:-144px 0;} -.icon-user{background-position:-168px 0;} -.icon-film{background-position:-192px 0;} -.icon-th-large{background-position:-216px 0;} -.icon-th{background-position:-240px 0;} -.icon-th-list{background-position:-264px 0;} -.icon-ok{background-position:-288px 0;} -.icon-remove{background-position:-312px 0;} -.icon-zoom-in{background-position:-336px 0;} -.icon-zoom-out{background-position:-360px 0;} -.icon-off{background-position:-384px 0;} -.icon-signal{background-position:-408px 0;} -.icon-cog{background-position:-432px 0;} -.icon-trash{background-position:-456px 0;} -.icon-home{background-position:0 -24px;} -.icon-file{background-position:-24px -24px;} -.icon-time{background-position:-48px -24px;} -.icon-road{background-position:-72px -24px;} -.icon-download-alt{background-position:-96px -24px;} -.icon-download{background-position:-120px -24px;} -.icon-upload{background-position:-144px -24px;} -.icon-inbox{background-position:-168px -24px;} -.icon-play-circle{background-position:-192px -24px;} -.icon-repeat{background-position:-216px -24px;} -.icon-refresh{background-position:-240px -24px;} -.icon-list-alt{background-position:-264px -24px;} -.icon-lock{background-position:-287px -24px;} -.icon-flag{background-position:-312px -24px;} -.icon-headphones{background-position:-336px -24px;} -.icon-volume-off{background-position:-360px -24px;} -.icon-volume-down{background-position:-384px -24px;} -.icon-volume-up{background-position:-408px -24px;} -.icon-qrcode{background-position:-432px -24px;} -.icon-barcode{background-position:-456px -24px;} -.icon-tag{background-position:0 -48px;} -.icon-tags{background-position:-25px -48px;} -.icon-book{background-position:-48px -48px;} -.icon-bookmark{background-position:-72px -48px;} -.icon-print{background-position:-96px -48px;} -.icon-camera{background-position:-120px -48px;} -.icon-font{background-position:-144px -48px;} -.icon-bold{background-position:-167px -48px;} -.icon-italic{background-position:-192px -48px;} -.icon-text-height{background-position:-216px -48px;} -.icon-text-width{background-position:-240px -48px;} -.icon-align-left{background-position:-264px -48px;} -.icon-align-center{background-position:-288px -48px;} -.icon-align-right{background-position:-312px -48px;} -.icon-align-justify{background-position:-336px -48px;} -.icon-list{background-position:-360px -48px;} -.icon-indent-left{background-position:-384px -48px;} -.icon-indent-right{background-position:-408px -48px;} -.icon-facetime-video{background-position:-432px -48px;} -.icon-picture{background-position:-456px -48px;} -.icon-pencil{background-position:0 -72px;} -.icon-map-marker{background-position:-24px -72px;} -.icon-adjust{background-position:-48px -72px;} -.icon-tint{background-position:-72px -72px;} -.icon-edit{background-position:-96px -72px;} -.icon-share{background-position:-120px -72px;} -.icon-check{background-position:-144px -72px;} -.icon-move{background-position:-168px -72px;} -.icon-step-backward{background-position:-192px -72px;} -.icon-fast-backward{background-position:-216px -72px;} -.icon-backward{background-position:-240px -72px;} -.icon-play{background-position:-264px -72px;} -.icon-pause{background-position:-288px -72px;} -.icon-stop{background-position:-312px -72px;} -.icon-forward{background-position:-336px -72px;} -.icon-fast-forward{background-position:-360px -72px;} -.icon-step-forward{background-position:-384px -72px;} -.icon-eject{background-position:-408px -72px;} -.icon-chevron-left{background-position:-432px -72px;} -.icon-chevron-right{background-position:-456px -72px;} -.icon-plus-sign{background-position:0 -96px;} -.icon-minus-sign{background-position:-24px -96px;} -.icon-remove-sign{background-position:-48px -96px;} -.icon-ok-sign{background-position:-72px -96px;} -.icon-question-sign{background-position:-96px -96px;} -.icon-info-sign{background-position:-120px -96px;} -.icon-screenshot{background-position:-144px -96px;} -.icon-remove-circle{background-position:-168px -96px;} -.icon-ok-circle{background-position:-192px -96px;} -.icon-ban-circle{background-position:-216px -96px;} -.icon-arrow-left{background-position:-240px -96px;} -.icon-arrow-right{background-position:-264px -96px;} -.icon-arrow-up{background-position:-289px -96px;} -.icon-arrow-down{background-position:-312px -96px;} -.icon-share-alt{background-position:-336px -96px;} -.icon-resize-full{background-position:-360px -96px;} -.icon-resize-small{background-position:-384px -96px;} -.icon-plus{background-position:-408px -96px;} -.icon-minus{background-position:-433px -96px;} -.icon-asterisk{background-position:-456px -96px;} -.icon-exclamation-sign{background-position:0 -120px;} -.icon-gift{background-position:-24px -120px;} -.icon-leaf{background-position:-48px -120px;} -.icon-fire{background-position:-72px -120px;} -.icon-eye-open{background-position:-96px -120px;} -.icon-eye-close{background-position:-120px -120px;} -.icon-warning-sign{background-position:-144px -120px;} -.icon-plane{background-position:-168px -120px;} -.icon-calendar{background-position:-192px -120px;} -.icon-random{background-position:-216px -120px;} -.icon-comment{background-position:-240px -120px;} -.icon-magnet{background-position:-264px -120px;} -.icon-chevron-up{background-position:-288px -120px;} -.icon-chevron-down{background-position:-313px -119px;} -.icon-retweet{background-position:-336px -120px;} -.icon-shopping-cart{background-position:-360px -120px;} -.icon-folder-close{background-position:-384px -120px;} -.icon-folder-open{background-position:-408px -120px;} -.icon-resize-vertical{background-position:-432px -119px;} -.icon-resize-horizontal{background-position:-456px -118px;} -.dropdown{position:relative;} -.dropdown-toggle{*margin-bottom:-3px;} -.dropdown-toggle:active,.open .dropdown-toggle{outline:0;} -.caret{display:inline-block;width:0;height:0;text-indent:-99999px;*text-indent:0;vertical-align:top;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000000;opacity:0.3;filter:alpha(opacity=30);content:"\2193";} -.dropdown .caret{margin-top:8px;margin-left:2px;} -.dropdown:hover .caret,.open.dropdown .caret{opacity:1;filter:alpha(opacity=100);} -.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;float:left;display:none;min-width:160px;_width:160px;padding:4px 0;margin:0;list-style:none;background-color:#ffffff;border-color:#ccc;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:1px;-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;*border-right-width:2px;*border-bottom-width:2px;}.dropdown-menu.bottom-up{top:auto;bottom:100%;margin-bottom:2px;} -.dropdown-menu .divider{height:1px;margin:5px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;} -.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:18px;color:#555555;white-space:nowrap;} -.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;background-color:#0088cc;} -.dropdown.open{*z-index:1000;}.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);} -.dropdown.open .dropdown-menu{display:block;} -.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} -.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);} -.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;} -.collapse{-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;position:relative;overflow:hidden;height:0;}.collapse.in{height:auto;} -.close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;opacity:0.4;filter:alpha(opacity=40);cursor:pointer;} -.btn{display:inline-block;padding:4px 10px 4px;margin-bottom:0;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);*margin-left:.3em;}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;} -.btn:active,.btn.active{background-color:#cccccc \9;} -.btn:first-child{*margin-left:0;} -.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;} -.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} -.btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;outline:0;} -.btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} -.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} -.btn-large [class^="icon-"]{margin-top:1px;} -.btn-small{padding:5px 9px;font-size:11px;line-height:16px;} -.btn-small [class^="icon-"]{margin-top:-1px;} -.btn-mini{padding:2px 6px;font-size:11px;line-height:14px;} -.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;} -.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-dark.active{color:rgba(255, 255, 255, 0.75);} -.btn-primary{background-color:#006dcc;background-image:-moz-linear-gradient(top, #0088cc, #0044cc);background-image:-ms-linear-gradient(top, #0088cc, #0044cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));background-image:-webkit-linear-gradient(top, #0088cc, #0044cc);background-image:-o-linear-gradient(top, #0088cc, #0044cc);background-image:linear-gradient(top, #0088cc, #0044cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);border-color:#0044cc #0044cc #002a80;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#0044cc;} -.btn-primary:active,.btn-primary.active{background-color:#003399 \9;} -.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;} -.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;} -.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;} -.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;} -.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;} -.btn-success:active,.btn-success.active{background-color:#408140 \9;} -.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;} -.btn-info:active,.btn-info.active{background-color:#24748c \9;} -.btn-inverse{background-color:#393939;background-image:-moz-linear-gradient(top, #454545, #262626);background-image:-ms-linear-gradient(top, #454545, #262626);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#454545), to(#262626));background-image:-webkit-linear-gradient(top, #454545, #262626);background-image:-o-linear-gradient(top, #454545, #262626);background-image:linear-gradient(top, #454545, #262626);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#454545', endColorstr='#262626', GradientType=0);border-color:#262626 #262626 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#262626;} -.btn-inverse:active,.btn-inverse.active{background-color:#0c0c0c \9;} -button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;} -button.btn.large,input[type="submit"].btn.large{*padding-top:7px;*padding-bottom:7px;} -button.btn.small,input[type="submit"].btn.small{*padding-top:3px;*padding-bottom:3px;} -.btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";} -.btn-group:after{clear:both;} -.btn-group:first-child{*margin-left:0;} -.btn-group+.btn-group{margin-left:5px;} -.btn-toolbar{margin-top:9px;margin-bottom:9px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;} -.btn-group .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} -.btn-group .btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;} -.btn-group .btn:last-child,.btn-group .dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;} -.btn-group .btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;} -.btn-group .btn.large:last-child,.btn-group .large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;} -.btn-group .btn:hover,.btn-group .btn:focus,.btn-group .btn:active,.btn-group .btn.active{z-index:2;} -.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;} -.btn-group .dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:5px;*padding-bottom:5px;} -.btn-group.open{*z-index:1000;}.btn-group.open .dropdown-menu{display:block;margin-top:1px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} -.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);} -.btn .caret{margin-top:7px;margin-left:0;} -.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);} -.btn-primary .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);} -.btn-small .caret{margin-top:4px;} -.alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} -.alert,.alert-heading{color:#c09853;} -.alert .close{position:relative;top:-2px;right:-21px;line-height:18px;} -.alert-success{background-color:#dff0d8;border-color:#d6e9c6;} -.alert-success,.alert-success .alert-heading{color:#468847;} -.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;} -.alert-danger,.alert-error,.alert-danger .alert-heading,.alert-error .alert-heading{color:#b94a48;} -.alert-info{background-color:#d9edf7;border-color:#bce8f1;} -.alert-info,.alert-info .alert-heading{color:#3a87ad;} -.alert-block{padding-top:14px;padding-bottom:14px;} -.alert-block>p,.alert-block>ul{margin-bottom:0;} -.alert-block p+p{margin-top:5px;} -.nav{margin-left:0;margin-bottom:18px;list-style:none;} -.nav>li>a{display:block;} -.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;} -.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;} -.nav li+.nav-header{margin-top:9px;} -.nav-list{padding-left:14px;padding-right:14px;margin-bottom:0;} -.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);} -.nav-list>li>a{padding:3px 15px;} -.nav-list .active>a,.nav-list .active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;} -.nav-list [class^="icon-"]{margin-right:2px;} -.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";} -.nav-tabs:after,.nav-pills:after{clear:both;} -.nav-tabs>li,.nav-pills>li{float:left;} -.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;} -.nav-tabs{border-bottom:1px solid #ddd;} -.nav-tabs>li{margin-bottom:-1px;} -.nav-tabs>li>a{padding-top:9px;padding-bottom:9px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;} -.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;} -.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} -.nav-pills .active>a,.nav-pills .active>a:hover{color:#ffffff;background-color:#0088cc;} -.nav-stacked>li{float:none;} -.nav-stacked>li>a{margin-right:0;} -.nav-tabs.nav-stacked{border-bottom:0;} -.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} -.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;} -.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;} -.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;} -.nav-pills.nav-stacked>li>a{margin-bottom:3px;} -.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;} -.nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;} -.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} -.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#0088cc;margin-top:6px;} -.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;} -.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;} -.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;} -.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;} -.nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;opacity:1;filter:alpha(opacity=100);} -.tabs-stacked .open>a:hover{border-color:#999999;} -.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";} -.tabbable:after{clear:both;} -.tab-content{overflow:hidden;} -.tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;} -.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;} -.tab-content>.active,.pill-content>.active{display:block;} -.tabs-below .nav-tabs{border-top:1px solid #ddd;} -.tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;} -.tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;} -.tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;} -.tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;} -.tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;} -.tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;} -.tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;} -.tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;} -.tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;} -.tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;} -.tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;} -.tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;} -.tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;} -.navbar{overflow:visible;margin-bottom:18px;} -.navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);} -.btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;} -.btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;} -.btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);} -.btn-navbar .icon-bar+.icon-bar{margin-top:3px;} -.nav-collapse.collapse{height:auto;} -.navbar .brand:hover{text-decoration:none;} -.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;} -.navbar .navbar-text{margin-bottom:0;line-height:40px;color:#999999;}.navbar .navbar-text a:hover{color:#ffffff;background-color:transparent;} -.navbar .btn,.navbar .btn-group{margin-top:5px;} -.navbar .btn-group .btn{margin-top:0;} -.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";} -.navbar-form:after{clear:both;} -.navbar-form input,.navbar-form select{display:inline-block;margin-top:5px;margin-bottom:0;} -.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;} -.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;} -.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;} -.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;color:rgba(255, 255, 255, 0.75);background:#666;background:rgba(255, 255, 255, 0.3);border:1px solid #111;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query :-moz-placeholder{color:#eeeeee;} -.navbar-search .search-query::-webkit-input-placeholder{color:#eeeeee;} -.navbar-search .search-query:hover{color:#ffffff;background-color:#999999;background-color:rgba(255, 255, 255, 0.5);} -.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;} -.navbar-fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030;} -.navbar-fixed-top .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} -.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;} -.navbar .nav.pull-right{float:right;} -.navbar .nav>li{display:block;float:left;} -.navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);} -.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;} -.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;} -.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;} -.navbar .nav.pull-right{margin-left:10px;margin-right:0;} -.navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;} -.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;} -.navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;} -.navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);} -.navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;} -.navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;} -.navbar .nav.pull-right .dropdown-menu{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before{left:auto;right:12px;} -.navbar .nav.pull-right .dropdown-menu:after{left:auto;right:13px;} -.breadcrumb{padding:7px 14px;margin:0 0 18px;background-color:#fbfbfb;background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline-block;text-shadow:0 1px 0 #ffffff;} -.breadcrumb .divider{padding:0 5px;color:#999999;} -.breadcrumb .active a{color:#333333;} -.pagination{height:36px;margin:18px 0;} -.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);} -.pagination li{display:inline;} -.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0;} -.pagination a:hover,.pagination .active a{background-color:#f5f5f5;} -.pagination .active a{color:#999999;cursor:default;} -.pagination .disabled a,.pagination .disabled a:hover{color:#999999;background-color:transparent;cursor:default;} -.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} -.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} -.pagination-centered{text-align:center;} -.pagination-right{text-align:right;} -.pager{margin-left:0;margin-bottom:18px;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";} -.pager:after{clear:both;} -.pager li{display:inline;} -.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;} -.pager a:hover{text-decoration:none;background-color:#f5f5f5;} -.pager .next a{float:right;} -.pager .previous a{float:left;} -.modal-open .dropdown-menu{z-index:2050;} -.modal-open .dropdown.open{*z-index:2050;} -.modal-open .popover{z-index:2060;} -.modal-open .tooltip{z-index:2070;} -.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;} -.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);} -.modal{position:fixed;top:50%;left:50%;z-index:1050;max-height:500px;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;} -.modal.fade.in{top:50%;} -.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;} -.modal-body{padding:15px;} -.modal-body .modal-form{margin-bottom:0;} -.modal-footer{padding:14px 15px 15px;margin-bottom:0;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";} -.modal-footer:after{clear:both;} -.modal-footer .btn{float:right;margin-left:5px;margin-bottom:0;} -.tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);} -.tooltip.top{margin-top:-2px;} -.tooltip.right{margin-left:2px;} -.tooltip.bottom{margin-top:2px;} -.tooltip.left{margin-left:-2px;} -.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} -.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} -.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} -.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} -.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} -.tooltip-arrow{position:absolute;width:0;height:0;} -.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px;}.popover.top{margin-top:-5px;} -.popover.right{margin-left:5px;} -.popover.bottom{margin-top:5px;} -.popover.left{margin-left:-5px;} -.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} -.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} -.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} -.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} -.popover .arrow{position:absolute;width:0;height:0;} -.popover-inner{padding:3px;width:280px;overflow:hidden;background:#000000;background:rgba(0, 0, 0, 0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);} -.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;} -.popover-content{padding:14px;background-color:#ffffff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;} -.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";} -.thumbnails:after{clear:both;} -.thumbnails>li{float:left;margin:0 0 18px 20px;} -.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);} -a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);} -.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;} -.thumbnail .caption{padding:9px;} -.label{padding:2px 4px 3px;font-size:11.049999999999999px;font-weight:bold;color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} -.label:hover{color:#ffffff;text-decoration:none;} -.label-important{background-color:#b94a48;} -.label-important:hover{background-color:#953b39;} -.label-warning{background-color:#f89406;} -.label-warning:hover{background-color:#c67605;} -.label-success{background-color:#468847;} -.label-success:hover{background-color:#356635;} -.label-info{background-color:#3a87ad;} -.label-info:hover{background-color:#2d6987;} -@-webkit-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}.progress{overflow:hidden;height:18px;margin-bottom:18px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-ms-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(top, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} -.progress .bar{width:0%;height:18px;color:#ffffff;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-ms-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(top, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-ms-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;} -.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;} -.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;} -.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);} -.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} -.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);} -.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} -.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);} -.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} -.accordion{margin-bottom:18px;} -.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} -.accordion-heading{border-bottom:0;} -.accordion-heading .accordion-toggle{display:block;padding:8px 15px;} -.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;} -.carousel{position:relative;margin-bottom:18px;line-height:1;} -.carousel-inner{overflow:hidden;width:100%;position:relative;} -.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-ms-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;} -.carousel .item>img{display:block;line-height:1;} -.carousel .active,.carousel .next,.carousel .prev{display:block;} -.carousel .active{left:0;} -.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;} -.carousel .next{left:100%;} -.carousel .prev{left:-100%;} -.carousel .next.left,.carousel .prev.right{left:0;} -.carousel .active.left{left:-100%;} -.carousel .active.right{left:100%;} -.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;} -.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);} -.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:10px 15px 5px;background:#333333;background:rgba(0, 0, 0, 0.75);} -.carousel-caption h4,.carousel-caption p{color:#ffffff;} -.hero-unit{padding:60px;margin-bottom:30px;background-color:#f5f5f5;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;} -.hero-unit p{font-size:18px;font-weight:200;line-height:27px;} -.pull-right{float:right;} -.pull-left{float:left;} -.hide{display:none;} -.show{display:block;} -.invisible{visibility:hidden;} diff --git a/apigen/templates/woodocs/resources/code-line.png b/apigen/templates/woodocs/resources/code-line.png deleted file mode 100644 index 42f00b66f6a..00000000000 Binary files a/apigen/templates/woodocs/resources/code-line.png and /dev/null differ diff --git a/apigen/templates/woodocs/resources/combined.js b/apigen/templates/woodocs/resources/combined.js deleted file mode 100644 index b40031373a9..00000000000 --- a/apigen/templates/woodocs/resources/combined.js +++ /dev/null @@ -1,1275 +0,0 @@ - -var ApiGen = ApiGen || {}; -ApiGen.config = {"require":{"min":"2.8.0"},"resources":{"resources":"resources"},"templates":{"common":{"overview.latte":"index.html","combined.js.latte":"resources\/combined.js","elementlist.js.latte":"elementlist.js","404.latte":"404.html"},"optional":{"sitemap":{"filename":"sitemap.xml","template":"sitemap.xml.latte"},"opensearch":{"filename":"opensearch.xml","template":"opensearch.xml.latte"},"robots":{"filename":"robots.txt","template":"robots.txt.latte"}},"main":{"package":{"filename":"package-%s.html","template":"package.latte"},"namespace":{"filename":"namespace-%s.html","template":"namespace.latte"},"class":{"filename":"class-%s.html","template":"class.latte"},"constant":{"filename":"constant-%s.html","template":"constant.latte"},"function":{"filename":"function-%s.html","template":"function.latte"},"source":{"filename":"source-%s.html","template":"source.latte"},"tree":{"filename":"tree.html","template":"tree.latte"},"deprecated":{"filename":"deprecated.html","template":"deprecated.latte"},"todo":{"filename":"todo.html","template":"todo.latte"}}},"options":{"elementDetailsCollapsed":true,"elementsOrder":"natural"},"config":"\/usr\/local\/share\/pear\/templates\/woo\/config.neon"}; - - -/*! jQuery v1.7 jquery.com | jquery.org/license */ -(function(a,b){function cA(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cx(a){if(!cm[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cn||(cn=c.createElement("iframe"),cn.frameBorder=cn.width=cn.height=0),b.appendChild(cn);if(!co||!cn.createElement)co=(cn.contentWindow||cn.contentDocument).document,co.write((c.compatMode==="CSS1Compat"?"":"")+""),co.close();d=co.createElement(a),co.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cn)}cm[a]=e}return cm[a]}function cw(a,b){var c={};f.each(cs.concat.apply([],cs.slice(0,b)),function(){c[this]=a});return c}function cv(){ct=b}function cu(){setTimeout(cv,0);return ct=f.now()}function cl(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ck(){try{return new a.XMLHttpRequest}catch(b){}}function ce(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bB(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function br(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bi,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bq(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bp(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bp)}function bp(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bo(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bn(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bm(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(){return!0}function M(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z]|[0-9])/ig,x=/^-ms-/,y=function(a,b){return(b+"").toUpperCase()},z=d.userAgent,A,B,C,D=Object.prototype.toString,E=Object.prototype.hasOwnProperty,F=Array.prototype.push,G=Array.prototype.slice,H=String.prototype.trim,I=Array.prototype.indexOf,J={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7",length:0,size:function(){return this.length},toArray:function(){return G.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?F.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),B.add(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(G.apply(this,arguments),"slice",G.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:F,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;B.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!B){B=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",C,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",C),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&K()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return a!=null&&m.test(a)&&!isNaN(a)},type:function(a){return a==null?String(a):J[D.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!E.call(a,"constructor")&&!E.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||E.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(x,"ms-").replace(w,y)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,unknownElems:!!a.getElementsByTagName("nav").length,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",enctype:!!c.createElement("form").enctype,submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.lastChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},m&&f.extend(p,{position:"absolute",left:"-999px",top:"-999px"});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;f(function(){var a,b,d,e,g,h,i=1,j="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",l="visibility:hidden;border:0;",n="style='"+j+"border:5px solid #000;padding:0;'",p="
"+""+"
";m=c.getElementsByTagName("body")[0];!m||(a=c.createElement("div"),a.style.cssText=l+"width:0;height:0;position:static;top:0;margin-top:"+i+"px",m.insertBefore(a,m.firstChild),o=c.createElement("div"),o.style.cssText=j+l,o.innerHTML=p,a.appendChild(o),b=o.firstChild,d=b.firstChild,g=b.nextSibling.firstChild.firstChild,h={doesNotAddBorder:d.offsetTop!==5,doesAddBorderForTableAndCells:g.offsetTop===5},d.style.position="fixed",d.style.top="20px",h.fixedPosition=d.offsetTop===20||d.offsetTop===15,d.style.position=d.style.top="",b.style.overflow="hidden",b.style.position="relative",h.subtractsBorderForOverflowNotVisible=d.offsetTop===-5,h.doesNotIncludeMarginInBodyOffset=m.offsetTop!==i,m.removeChild(a),o=a=null,f.extend(k,h))}),o.innerHTML="",n.removeChild(o),o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[f.expando]:a[f.expando]&&f.expando,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[f.expando]=n=++f.uuid:n=f.expando),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[f.expando]:f.expando;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)?b=b:b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" "));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];if(!arguments.length){if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}return b}e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!a||j===3||j===8||j===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g},removeAttr:function(a,b){var c,d,e,g,h=0;if(a.nodeType===1){d=(b||"").split(p),g=d.length;for(;h=0}})});var z=/\.(.*)$/,A=/^(?:textarea|input|select)$/i,B=/\./g,C=/ /g,D=/[^\w\s.|`]/g,E=/^([^\.]*)?(?:\.(.+))?$/,F=/\bhover(\.\S+)?/,G=/^key/,H=/^(?:mouse|contextmenu)|click/,I=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,J=function(a){var b=I.exec(a);b&& -(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},K=function(a,b){return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||a.id===b[2])&&(!b[3]||b[3].test(a.className))},L=function(a){return f.event.special.hover?a:a.replace(F,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=L(c).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"",(g||!e)&&c.preventDefault();if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,n=null;for(m=e.parentNode;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l=0:t===b&&(t=o[s]=r.quick?K(m,r.quick):f(m).is(s)),t&&q.push(r);q.length&&j.push({elem:m,matches:q})}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),G.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),H.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var Y="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",Z=/ jQuery\d+="(?:\d+|null)"/g,$=/^\s+/,_=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,ba=/<([\w:]+)/,bb=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bk=X(c);bj.optgroup=bj.option,bj.tbody=bj.tfoot=bj.colgroup=bj.caption=bj.thead,bj.th=bj.td,f.support.htmlSerialize||(bj._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after" -,arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Z,""):null;if(typeof a=="string"&&!bd.test(a)&&(f.support.leadingWhitespace||!$.test(a))&&!bj[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(_,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bn(a,d),e=bo(a),g=bo(d);for(h=0;e[h];++h)g[h]&&bn(e[h],g[h])}if(b){bm(a,d);if(c){e=bo(a),g=bo(d);for(h=0;e[h];++h)bm(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!bc.test(k))k=b.createTextNode(k);else{k=k.replace(_,"<$1>");var l=(ba.exec(k)||["",""])[1].toLowerCase(),m=bj[l]||bj._default,n=m[0],o=b.createElement("div");b===c?bk.appendChild(o):X(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=bb.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&$.test(k)&&o.insertBefore(b.createTextNode($.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bt.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bs,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bs.test(g)?g.replace(bs,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bB(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bC=function(a,c){var d,e,g;c=c.replace(bu,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bD=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bv.test(f)&&bw.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bB=bC||bD,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bF=/%20/g,bG=/\[\]$/,bH=/\r?\n/g,bI=/#.*$/,bJ=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bK=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bL=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bM=/^(?:GET|HEAD)$/,bN=/^\/\//,bO=/\?/,bP=/)<[^<]*)*<\/script>/gi,bQ=/^(?:select|textarea)/i,bR=/\s+/,bS=/([?&])_=[^&]*/,bT=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bU=f.fn.load,bV={},bW={},bX,bY,bZ=["*/"]+["*"];try{bX=e.href}catch(b$){bX=c.createElement("a"),bX.href="",bX=bX.href}bY=bT.exec(bX.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bU)return bU.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bP,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bQ.test(this.nodeName)||bK.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bH,"\r\n")}}):{name:b.name,value:c.replace(bH,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?cb(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),cb(a,b);return a},ajaxSettings:{url:bX,isLocal:bL.test(bY[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bZ},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:b_(bV),ajaxTransport:b_(bW),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cd(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=ce(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bJ.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bI,"").replace(bN,bY[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bR),d.crossDomain==null&&(r=bT.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bY[1]&&r[2]==bY[2]&&(r[3]||(r[1]==="http:"?80:443))==(bY[3]||(bY[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),ca(bV,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bM.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bO.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bS,"$1_="+x);d.url=y+(y===d.url?(bO.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bZ+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=ca(bW,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){s<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)cc(g,a[g],c,e);return d.join("&").replace(bF,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cf=f.now(),cg=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cf++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cg.test(b.url)||e&&cg.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cg,l),b.url===j&&(e&&(k=k.replace(cg,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ch=a.ActiveXObject?function(){for(var a in cj)cj[a](0,1)}:!1,ci=0,cj;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ck()||cl()}:ck,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ch&&delete cj[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++ci,ch&&(cj||(cj={},f(a).unload(ch)),cj[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cm={},cn,co,cp=/^(?:toggle|show|hide)$/,cq=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cr,cs=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],ct;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cw("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cz.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cz.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cA(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cA(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); -/** - * Cookie plugin - * - * Copyright (c) 2006 Klaus Hartl (stilbuero.de) - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - */ - -/** - * Create a cookie with the given name and value and other optional parameters. - * - * @example $.cookie('the_cookie', 'the_value'); - * @desc Set the value of a cookie. - * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true }); - * @desc Create a cookie with all available options. - * @example $.cookie('the_cookie', 'the_value'); - * @desc Create a session cookie. - * @example $.cookie('the_cookie', null); - * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain - * used when the cookie was set. - * - * @param String name The name of the cookie. - * @param String value The value of the cookie. - * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. - * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. - * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. - * If set to null or omitted, the cookie will be a session cookie and will not be retained - * when the the browser exits. - * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). - * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). - * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will - * require a secure protocol (like HTTPS). - * @type undefined - * - * @name $.cookie - * @cat Plugins/Cookie - * @author Klaus Hartl/klaus.hartl@stilbuero.de - */ - -/** - * Get the value of a cookie with the given name. - * - * @example $.cookie('the_cookie'); - * @desc Get the value of a cookie. - * - * @param String name The name of the cookie. - * @return The value of the cookie. - * @type String - * - * @name $.cookie - * @cat Plugins/Cookie - * @author Klaus Hartl/klaus.hartl@stilbuero.de - */ -jQuery.cookie = function(name, value, options) { - if (typeof value != 'undefined') { // name and value given, set cookie - options = options || {}; - if (value === null) { - value = ''; - options.expires = -1; - } - var expires = ''; - if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { - var date; - if (typeof options.expires == 'number') { - date = new Date(); - date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); - } else { - date = options.expires; - } - expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE - } - // CAUTION: Needed to parenthesize options.path and options.domain - // in the following expressions, otherwise they evaluate to undefined - // in the packed version for some reason... - var path = options.path ? '; path=' + (options.path) : ''; - var domain = options.domain ? '; domain=' + (options.domain) : ''; - var secure = options.secure ? '; secure' : ''; - document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); - } else { // only name given, get cookie - var cookieValue = null; - if (document.cookie && document.cookie != '') { - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var cookie = jQuery.trim(cookies[i]); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) == (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; - } -}; -/*! - * sprintf and vsprintf for jQuery - * somewhat based on http://jan.moesen.nu/code/javascript/sprintf-and-printf-in-javascript/ - * Copyright (c) 2008 Sabin Iacob (m0n5t3r) - * @license http://www.gnu.org/licenses/gpl.html - * @project jquery.sprintf - */ -(function(d){var a={b:function(e){return parseInt(e,10).toString(2)},c:function(e){return String.fromCharCode(parseInt(e,10))},d:function(e){return parseInt(e,10)},u:function(e){return Math.abs(e)},f:function(f,e){e=parseInt(e,10);f=parseFloat(f);if(isNaN(e&&f)){return NaN}return e&&f.toFixed(e)||f},o:function(e){return parseInt(e,10).toString(8)},s:function(e){return e},x:function(e){return(""+parseInt(e,10).toString(16)).toLowerCase()},X:function(e){return(""+parseInt(e,10).toString(16)).toUpperCase()}};var c=/%(?:(\d+)?(?:\.(\d+))?|\(([^)]+)\))([%bcdufosxX])/g;var b=function(f){if(f.length==1&&typeof f[0]=="object"){f=f[0];return function(i,h,k,j,g,m,l){return a[g](f[j])}}else{var e=0;return function(i,h,k,j,g,m,l){if(g=="%"){return"%"}return a[g](f[e++],k)}}};d.extend({sprintf:function(f){var e=Array.apply(null,arguments).slice(1);return f.replace(c,b(e))},vsprintf:function(f,e){return f.replace(c,b(e))}})})(jQuery); - -/*! - * jQuery Autocomplete plugin 1.1 - * - * Copyright (c) 2009 Jörn Zaefferer - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $ - */ - -;(function($) { - -$.fn.extend({ - autocomplete: function(urlOrData, options) { - var isUrl = typeof urlOrData == "string"; - options = $.extend({}, $.Autocompleter.defaults, { - url: isUrl ? urlOrData : null, - data: isUrl ? null : urlOrData, - delay: isUrl ? $.Autocompleter.defaults.delay : 10, - max: options && !options.scroll ? 10 : 150 - }, options); - - // if highlight is set to false, replace it with a do-nothing function - options.highlight = options.highlight || function(value) { return value; }; - - // if the formatMatch option is not specified, then use formatItem for backwards compatibility - options.formatMatch = options.formatMatch || options.formatItem; - - options.show = options.show || function(list) {}; - - return this.each(function() { - new $.Autocompleter(this, options); - }); - }, - result: function(handler) { - return this.bind("result", handler); - }, - search: function(handler) { - return this.trigger("search", [handler]); - }, - flushCache: function() { - return this.trigger("flushCache"); - }, - setOptions: function(options){ - return this.trigger("setOptions", [options]); - }, - unautocomplete: function() { - return this.trigger("unautocomplete"); - } -}); - -$.Autocompleter = function(input, options) { - - var KEY = { - UP: 38, - DOWN: 40, - DEL: 46, - TAB: 9, - RETURN: 13, - ESC: 27, - COMMA: 188, - PAGEUP: 33, - PAGEDOWN: 34, - BACKSPACE: 8 - }; - - // Create $ object for input element - var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); - - var timeout; - var previousValue = ""; - var cache = $.Autocompleter.Cache(options); - var hasFocus = 0; - var lastKeyPressCode; - var config = { - mouseDownOnSelect: false - }; - var select = $.Autocompleter.Select(options, input, selectCurrent, config); - - // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all - $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) { - // a keypress means the input has focus - // avoids issue where input had focus before the autocomplete was applied - hasFocus = 1; - // track last key pressed - lastKeyPressCode = event.keyCode; - switch(event.keyCode) { - - case KEY.UP: - event.preventDefault(); - if ( select.visible() ) { - select.prev(); - } else { - onChange(0, true); - } - break; - - case KEY.DOWN: - event.preventDefault(); - if ( select.visible() ) { - select.next(); - } else { - onChange(0, true); - } - break; - - case KEY.PAGEUP: - event.preventDefault(); - if ( select.visible() ) { - select.pageUp(); - } else { - onChange(0, true); - } - break; - - case KEY.PAGEDOWN: - event.preventDefault(); - if ( select.visible() ) { - select.pageDown(); - } else { - onChange(0, true); - } - break; - - // matches also semicolon - case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: - case KEY.TAB: - case KEY.RETURN: - if( selectCurrent() ) { - //event.preventDefault(); - //return false; - } - break; - - case KEY.ESC: - select.hide(); - break; - - default: - clearTimeout(timeout); - timeout = setTimeout(onChange, options.delay); - break; - } - }).focus(function(){ - // track whether the field has focus, we shouldn't process any - // results if the field no longer has focus - hasFocus++; - }).blur(function() { - hasFocus = 0; - if (!config.mouseDownOnSelect) { - hideResults(); - } - }).click(function() { - // show select when clicking in a focused field - if ( hasFocus++ > 1 && !select.visible() ) { - onChange(0, true); - } - }).bind("search", function() { - // TODO why not just specifying both arguments? - var fn = (arguments.length > 1) ? arguments[1] : null; - function findValueCallback(q, data) { - var result; - if( data && data.length ) { - for (var i=0; i < data.length; i++) { - if( data[i].result.toLowerCase() == q.toLowerCase() ) { - result = data[i]; - break; - } - } - } - if( typeof fn == "function" ) fn(result); - else $input.trigger("result", result && [result.data, result.value]); - } - $.each(trimWords($input.val()), function(i, value) { - request(value, findValueCallback, findValueCallback); - }); - }).bind("flushCache", function() { - cache.flush(); - }).bind("setOptions", function() { - $.extend(options, arguments[1]); - // if we've updated the data, repopulate - if ( "data" in arguments[1] ) - cache.populate(); - }).bind("unautocomplete", function() { - select.unbind(); - $input.unbind(); - $(input.form).unbind(".autocomplete"); - }); - - - function selectCurrent() { - var selected = select.selected(); - if( !selected ) - return false; - - var v = selected.result; - previousValue = v; - - if ( options.multiple ) { - var words = trimWords($input.val()); - if ( words.length > 1 ) { - var seperator = options.multipleSeparator.length; - var cursorAt = $(input).selection().start; - var wordAt, progress = 0; - $.each(words, function(i, word) { - progress += word.length; - if (cursorAt <= progress) { - wordAt = i; - return false; - } - progress += seperator; - }); - words[wordAt] = v; - // TODO this should set the cursor to the right position, but it gets overriden somewhere - //$.Autocompleter.Selection(input, progress + seperator, progress + seperator); - v = words.join( options.multipleSeparator ); - } - v += options.multipleSeparator; - } - - $input.val(v); - hideResultsNow(); - $input.trigger("result", [selected.data, selected.value]); - return true; - } - - function onChange(crap, skipPrevCheck) { - if( lastKeyPressCode == KEY.DEL ) { - select.hide(); - return; - } - - var currentValue = $input.val(); - - if ( !skipPrevCheck && currentValue == previousValue ) - return; - - previousValue = currentValue; - - currentValue = lastWord(currentValue); - if ( currentValue.length >= options.minChars) { - $input.addClass(options.loadingClass); - if (!options.matchCase) - currentValue = currentValue.toLowerCase(); - request(currentValue, receiveData, hideResultsNow); - } else { - stopLoading(); - select.hide(); - } - }; - - function trimWords(value) { - if (!value) - return [""]; - if (!options.multiple) - return [$.trim(value)]; - return $.map(value.split(options.multipleSeparator), function(word) { - return $.trim(value).length ? $.trim(word) : null; - }); - } - - function lastWord(value) { - if ( !options.multiple ) - return value; - var words = trimWords(value); - if (words.length == 1) - return words[0]; - var cursorAt = $(input).selection().start; - if (cursorAt == value.length) { - words = trimWords(value) - } else { - words = trimWords(value.replace(value.substring(cursorAt), "")); - } - return words[words.length - 1]; - } - - // fills in the input box w/the first match (assumed to be the best match) - // q: the term entered - // sValue: the first matching result - function autoFill(q, sValue){ - // autofill in the complete box w/the first match as long as the user hasn't entered in more data - // if the last user key pressed was backspace, don't autofill - if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) { - // fill in the value (keep the case the user has typed) - $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); - // select the portion of the value not typed by the user (so the next character will erase) - $(input).selection(previousValue.length, previousValue.length + sValue.length); - } - }; - - function hideResults() { - clearTimeout(timeout); - timeout = setTimeout(hideResultsNow, 200); - }; - - function hideResultsNow() { - var wasVisible = select.visible(); - select.hide(); - clearTimeout(timeout); - stopLoading(); - if (options.mustMatch) { - // call search and run callback - $input.search( - function (result){ - // if no value found, clear the input box - if( !result ) { - if (options.multiple) { - var words = trimWords($input.val()).slice(0, -1); - $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") ); - } - else { - $input.val( "" ); - $input.trigger("result", null); - } - } - } - ); - } - }; - - function receiveData(q, data) { - if ( data && data.length && hasFocus ) { - stopLoading(); - select.display(data, q); - autoFill(q, data[0].value); - select.show(); - } else { - hideResultsNow(); - } - }; - - function request(term, success, failure) { - if (!options.matchCase) - term = term.toLowerCase(); - var data = cache.load(term); - // recieve the cached data - if (data && data.length) { - success(term, data); - // if an AJAX url has been supplied, try loading the data now - } else if( (typeof options.url == "string") && (options.url.length > 0) ){ - - var extraParams = { - timestamp: +new Date() - }; - $.each(options.extraParams, function(key, param) { - extraParams[key] = typeof param == "function" ? param() : param; - }); - - $.ajax({ - // try to leverage ajaxQueue plugin to abort previous requests - mode: "abort", - // limit abortion to this input - port: "autocomplete" + input.name, - dataType: options.dataType, - url: options.url, - data: $.extend({ - q: lastWord(term), - limit: options.max - }, extraParams), - success: function(data) { - var parsed = options.parse && options.parse(data) || parse(data); - cache.add(term, parsed); - success(term, parsed); - } - }); - } else { - // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match - select.emptyList(); - failure(term); - } - }; - - function parse(data) { - var parsed = []; - var rows = data.split("\n"); - for (var i=0; i < rows.length; i++) { - var row = $.trim(rows[i]); - if (row) { - row = row.split("|"); - parsed[parsed.length] = { - data: row, - value: row[0], - result: options.formatResult && options.formatResult(row, row[0]) || row[0] - }; - } - } - return parsed; - }; - - function stopLoading() { - $input.removeClass(options.loadingClass); - }; - -}; - -$.Autocompleter.defaults = { - inputClass: "ac_input", - resultsClass: "ac_results", - loadingClass: "ac_loading", - minChars: 1, - delay: 400, - matchCase: false, - matchSubset: true, - matchContains: false, - cacheLength: 10, - max: 100, - mustMatch: false, - extraParams: {}, - selectFirst: true, - formatItem: function(row) { return row[0]; }, - formatMatch: null, - autoFill: false, - width: 0, - multiple: false, - multipleSeparator: ", ", - highlight: function(value, term) { - return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); - }, - scroll: true, - scrollHeight: 180 -}; - -$.Autocompleter.Cache = function(options) { - - var data = {}; - var length = 0; - - function matchSubset(s, sub) { - if (!options.matchCase) - s = s.toLowerCase(); - var i = s.indexOf(sub); - if (options.matchContains == "word"){ - i = s.toLowerCase().search("\\b" + sub.toLowerCase()); - } - if (i == -1) return false; - return i == 0 || options.matchContains; - }; - - function add(q, value) { - if (length > options.cacheLength){ - flush(); - } - if (!data[q]){ - length++; - } - data[q] = value; - } - - function populate(){ - if( !options.data ) return false; - // track the matches - var stMatchSets = {}, - nullData = 0; - - // no url was specified, we need to adjust the cache length to make sure it fits the local data store - if( !options.url ) options.cacheLength = 1; - - // track all options for minChars = 0 - stMatchSets[""] = []; - - // loop through the array and create a lookup structure - for ( var i = 0, ol = options.data.length; i < ol; i++ ) { - var rawValue = options.data[i]; - // if rawValue is a string, make an array otherwise just reference the array - rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; - - var value = options.formatMatch(rawValue, i+1, options.data.length); - if ( value === false ) - continue; - - var firstChar = value.charAt(0).toLowerCase(); - // if no lookup array for this character exists, look it up now - if( !stMatchSets[firstChar] ) - stMatchSets[firstChar] = []; - - // if the match is a string - var row = { - value: value, - data: rawValue, - result: options.formatResult && options.formatResult(rawValue) || value - }; - - // push the current match into the set list - stMatchSets[firstChar].push(row); - - // keep track of minChars zero items - if ( nullData++ < options.max ) { - stMatchSets[""].push(row); - } - }; - - // add the data items to the cache - $.each(stMatchSets, function(i, value) { - // increase the cache size - options.cacheLength++; - // add to the cache - add(i, value); - }); - } - - // populate any existing data - setTimeout(populate, 25); - - function flush(){ - data = {}; - length = 0; - } - - return { - flush: flush, - add: add, - populate: populate, - load: function(q) { - if (!options.cacheLength || !length) - return null; - /* - * if dealing w/local data and matchContains than we must make sure - * to loop through all the data collections looking for matches - */ - if( !options.url && options.matchContains ){ - // track all matches - var csub = []; - // loop through all the data grids for matches - for( var k in data ){ - // don't search through the stMatchSets[""] (minChars: 0) cache - // this prevents duplicates - if( k.length > 0 ){ - var c = data[k]; - $.each(c, function(i, x) { - // if we've got a match, add it to the array - if (matchSubset(x.value, q)) { - csub.push(x); - } - }); - } - } - return csub; - } else - // if the exact item exists, use it - if (data[q]){ - return data[q]; - } else - if (options.matchSubset) { - for (var i = q.length - 1; i >= options.minChars; i--) { - var c = data[q.substr(0, i)]; - if (c) { - var csub = []; - $.each(c, function(i, x) { - if (matchSubset(x.value, q)) { - csub[csub.length] = x; - } - }); - return csub; - } - } - } - return null; - } - }; -}; - -$.Autocompleter.Select = function (options, input, select, config) { - var CLASSES = { - ACTIVE: "ac_over" - }; - - var listItems, - active = -1, - data, - term = "", - needsInit = true, - element, - list; - - // Create results - function init() { - if (!needsInit) - return; - element = $("
") - .hide() - .addClass(options.resultsClass) - .css("position", "absolute") - .appendTo(document.body); - - list = $("
    ").appendTo(element).mouseover( function(event) { - if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { - active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); - $(target(event)).addClass(CLASSES.ACTIVE); - } - }).click(function(event) { - $(target(event)).addClass(CLASSES.ACTIVE); - select(); - // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus - input.focus(); - return false; - }).mousedown(function() { - config.mouseDownOnSelect = true; - }).mouseup(function() { - config.mouseDownOnSelect = false; - }); - - if( options.width > 0 ) - element.css("width", options.width); - - needsInit = false; - } - - function target(event) { - var element = event.target; - while(element && element.tagName != "LI") - element = element.parentNode; - // more fun with IE, sometimes event.target is empty, just ignore it then - if(!element) - return []; - return element; - } - - function moveSelect(step) { - listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); - movePosition(step); - var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); - if(options.scroll) { - var offset = 0; - listItems.slice(0, active).each(function() { - offset += this.offsetHeight; - }); - if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { - list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight()); - } else if(offset < list.scrollTop()) { - list.scrollTop(offset); - } - } - }; - - function movePosition(step) { - active += step; - if (active < 0) { - active = listItems.size() - 1; - } else if (active >= listItems.size()) { - active = 0; - } - } - - function limitNumberOfItems(available) { - return options.max && options.max < available - ? options.max - : available; - } - - function fillList() { - list.empty(); - var max = limitNumberOfItems(data.length); - for (var i=0; i < max; i++) { - if (!data[i]) - continue; - var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term); - if ( formatted === false ) - continue; - var li = $("
  • ").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0]; - $.data(li, "ac_data", data[i]); - } - listItems = list.find("li"); - if ( options.selectFirst ) { - listItems.slice(0, 1).addClass(CLASSES.ACTIVE); - active = 0; - } - // apply bgiframe if available - if ( $.fn.bgiframe ) - list.bgiframe(); - } - - return { - display: function(d, q) { - init(); - data = d; - term = q; - fillList(); - }, - next: function() { - moveSelect(1); - }, - prev: function() { - moveSelect(-1); - }, - pageUp: function() { - if (active != 0 && active - 8 < 0) { - moveSelect( -active ); - } else { - moveSelect(-8); - } - }, - pageDown: function() { - if (active != listItems.size() - 1 && active + 8 > listItems.size()) { - moveSelect( listItems.size() - 1 - active ); - } else { - moveSelect(8); - } - }, - hide: function() { - element && element.hide(); - listItems && listItems.removeClass(CLASSES.ACTIVE); - active = -1; - }, - visible : function() { - return element && element.is(":visible"); - }, - current: function() { - return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); - }, - show: function() { - var offset = $(input).offset(); - element.css({ - width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).innerWidth(), - top: offset.top + input.offsetHeight, - left: offset.left - }).show(); - options.show(element); - if(options.scroll) { - list.scrollTop(0); - list.css({ - maxHeight: options.scrollHeight, - overflow: 'auto' - }); - - if($.browser.msie && typeof document.body.style.maxHeight === "undefined") { - var listHeight = 0; - listItems.each(function() { - listHeight += this.offsetHeight; - }); - var scrollbarsVisible = listHeight > options.scrollHeight; - list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight ); - if (!scrollbarsVisible) { - // IE doesn't recalculate width when scrollbar disappears - listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) ); - } - } - - } - }, - selected: function() { - var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); - return selected && selected.length && $.data(selected[0], "ac_data"); - }, - emptyList: function (){ - list && list.empty(); - }, - unbind: function() { - element && element.remove(); - } - }; -}; - -$.fn.selection = function(start, end) { - if (start !== undefined) { - return this.each(function() { - if( this.createTextRange ){ - var selRange = this.createTextRange(); - if (end === undefined || start == end) { - selRange.move("character", start); - selRange.select(); - } else { - selRange.collapse(true); - selRange.moveStart("character", start); - selRange.moveEnd("character", end); - selRange.select(); - } - } else if( this.setSelectionRange ){ - this.setSelectionRange(start, end); - } else if( this.selectionStart ){ - this.selectionStart = start; - this.selectionEnd = end; - } - }); - } - var field = this[0]; - if ( field.createTextRange ) { - var range = document.selection.createRange(), - orig = field.value, - teststring = "<->", - textLength = range.text.length; - range.text = teststring; - var caretAt = field.value.indexOf(teststring); - field.value = orig; - this.selection(caretAt, caretAt + textLength); - return { - start: caretAt, - end: caretAt + textLength - } - } else if( field.selectionStart !== undefined ){ - return { - start: field.selectionStart, - end: field.selectionEnd - } - } -}; - -})(jQuery); -/** - * jQuery.fn.sortElements - * -------------- - * @author James Padolsey (http://james.padolsey.com) - * @version 0.11 - * @updated 18-MAR-2010 - * -------------- - * @param Function comparator: - * Exactly the same behaviour as [1,2,3].sort(comparator) - * - * @param Function getSortable - * A function that should return the element that is - * to be sorted. The comparator will run on the - * current collection, but you may want the actual - * resulting sort to occur on a parent or another - * associated element. - * - * E.g. $('td').sortElements(comparator, function(){ - * return this.parentNode; - * }) - * - * The
) will be sorted instead - * of the - - {foreach $class->annotations['todo'] as $description} - {sep}{/sep} - {/foreach} - - {/foreach} - {/define} - - {if $todoClasses} -

Classes summary

-
's parent (
itself. - */ -jQuery.fn.sortElements = (function(){ - - var sort = [].sort; - - return function(comparator, getSortable) { - - getSortable = getSortable || function(){return this;}; - - var placements = this.map(function(){ - - var sortElement = getSortable.call(this), - parentNode = sortElement.parentNode, - - // Since the element itself will change position, we have - // to have some way of storing it's original position in - // the DOM. The easiest way is to have a 'flag' node: - nextSibling = parentNode.insertBefore( - document.createTextNode(''), - sortElement.nextSibling - ); - - return function() { - - if (parentNode === this) { - throw new Error( - "You can't sort elements if any one is a descendant of another." - ); - } - - // Insert before flag: - parentNode.insertBefore(this, nextSibling); - // Remove flag: - parentNode.removeChild(nextSibling); - - }; - - }); - - return sort.call(this, comparator).each(function(i){ - placements[i].call(getSortable.call(this)); - }); - - }; - -})(); -/*! - * ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - * - * Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) - * Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) - * Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) - * Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - * - * For the full copyright and license information, please view - * the file LICENSE.md that was distributed with this source code. - */ - -$(function() { - var $document = $(document); - var $navigation = $('#navigation'); - var navigationHeight = $('#navigation').height(); - var $left = $('#left'); - var $right = $('#right'); - var $rightInner = $('#rightInner'); - var $splitter = $('#splitter'); - var $groups = $('#groups'); - var $content = $('#content'); - - // Menu - - // Hide deep packages and namespaces - $('ul span', $groups).click(function(event) { - event.preventDefault(); - event.stopPropagation(); - $(this) - .toggleClass('collapsed') - .parent() - .next('ul') - .toggleClass('collapsed'); - }).click(); - - $active = $('ul li.active', $groups); - if ($active.length > 0) { - // Open active - $('> a > span', $active).click(); - } else { - $main = $('> ul > li.main', $groups); - if ($main.length > 0) { - // Open first level of the main project - $('> a > span', $main).click(); - } else { - // Open first level of all - $('> ul > li > a > span', $groups).click(); - } - } - - // Content - - // Search autocompletion - var autocompleteFound = false; - var autocompleteFiles = {'c': 'class', 'co': 'constant', 'f': 'function', 'm': 'class', 'mm': 'class', 'p': 'class', 'mp': 'class', 'cc': 'class'}; - var $search = $('#search input[name=q]'); - $search - .autocomplete(ApiGen.elements, { - matchContains: true, - scrollHeight: 200, - max: 20, - formatItem: function(data) { - return data[1].replace(/^(.+\\)(.+)$/, '$1$2'); - }, - formatMatch: function(data) { - return data[1]; - }, - formatResult: function(data) { - return data[1]; - }, - show: function($list) { - var $items = $('li span', $list); - var maxWidth = Math.max.apply(null, $items.map(function() { - return $(this).width(); - })); - // 10px padding - $list.width(Math.max(maxWidth + 10, $search.innerWidth())); - } - }).result(function(event, data) { - autocompleteFound = true; - var location = window.location.href.split('/'); - location.pop(); - var parts = data[1].split(/::|$/); - var file = $.sprintf(ApiGen.config.templates.main[autocompleteFiles[data[0]]].filename, parts[0].replace(/[^\w]/g, '.')); - if (parts[1]) { - file += '#' + ('mm' === data[0] || 'mp' === data[0] ? 'm' : '') + parts[1].replace(/([\w]+)\(\)/, '_$1'); - } - location.push(file); - window.location = location.join('/'); - - // Workaround for Opera bug - $(this).closest('form').attr('action', location.join('/')); - }).closest('form') - .submit(function() { - var query = $search.val(); - if ('' === query) { - return false; - } - - var label = $('#search input[name=more]').val(); - if (!autocompleteFound && label && -1 === query.indexOf('more:')) { - $search.val(query + ' more:' + label); - } - - return !autocompleteFound && '' !== $('#search input[name=cx]').val(); - }); - - // Save natural order - $('table.summary tr[data-order]', $content).each(function(index) { - do { - index = '0' + index; - } while (index.length < 3); - $(this).attr('data-order-natural', index); - }); - - // Switch between natural and alphabetical order - var $caption = $('table.summary', $content) - .filter(':has(tr[data-order])') - .prev('h2'); - $caption - .click(function() { - var $this = $(this); - var order = $this.data('order') || 'natural'; - order = 'natural' === order ? 'alphabetical' : 'natural'; - $this.data('order', order); - $.cookie('order', order, {expires: 365}); - var attr = 'alphabetical' === order ? 'data-order' : 'data-order-natural'; - $this - .next('table') - .find('tr').sortElements(function(a, b) { - return $(a).attr(attr) > $(b).attr(attr) ? 1 : -1; - }); - return false; - }) - .addClass('switchable') - .attr('title', 'Switch between natural and alphabetical order'); - if ((null === $.cookie('order') && 'alphabetical' === ApiGen.config.options.elementsOrder) || 'alphabetical' === $.cookie('order')) { - $caption.click(); - } - - // Open details - if (ApiGen.config.options.elementDetailsCollapsed) { - $('tr', $content).filter(':has(.detailed)') - .click(function() { - var $this = $(this); - $('.short', $this).hide(); - $('.detailed', $this).show(); - }); - } - - // Splitter - var splitterWidth = $splitter.width(); - function setSplitterPosition(position) - { - $left.width(position); - $right.css('margin-left', position + splitterWidth); - $splitter.css('left', position); - } - function setNavigationPosition() - { - var height = $(window).height() - navigationHeight; - $left.height(height); - $splitter.height(height); - $right.height(height); - } - function setContentWidth() - { - var width = $rightInner.width(); - $rightInner - .toggleClass('medium', width <= 960) - .toggleClass('small', width <= 650); - } - $splitter.mousedown(function() { - $splitter.addClass('active'); - - $document.mousemove(function(event) { - if (event.pageX >= 230 && $document.width() - event.pageX >= 600 + splitterWidth) { - setSplitterPosition(event.pageX); - setContentWidth(); - } - }); - - $() - .add($splitter) - .add($document) - .mouseup(function() { - $splitter - .removeClass('active') - .unbind('mouseup'); - $document - .unbind('mousemove') - .unbind('mouseup'); - - $.cookie('splitter', parseInt($splitter.css('left')), {expires: 365}); - }); - - return false; - }); - var splitterPosition = $.cookie('splitter'); - if (null !== splitterPosition) { - setSplitterPosition(parseInt(splitterPosition)); - } - setNavigationPosition(); - setContentWidth(); - $(window) - .resize(setNavigationPosition) - .resize(setContentWidth); - - // Select selected lines - var matches = window.location.hash.substr(1).match(/^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/); - if (null !== matches) { - var lists = matches[0].split(','); - for (var i = 0; i < lists.length; i++) { - var lines = lists[i].split('-'); - lines[1] = lines[1] || lines[0]; - for (var j = lines[0]; j <= lines[1]; j++) { - $('#' + j).addClass('selected'); - } - } - - var $firstLine = $('#' + parseInt(matches[0])); - if ($firstLine.length > 0) { - $right.scrollTop($firstLine.offset().top); - } - } - - // Save selected lines - var lastLine; - $('a.l').click(function(event) { - event.preventDefault(); - - var $selectedLine = $(this).parent(); - var selectedLine = parseInt($selectedLine.attr('id')); - - if (event.shiftKey) { - if (lastLine) { - for (var i = Math.min(selectedLine, lastLine); i <= Math.max(selectedLine, lastLine); i++) { - $('#' + i).addClass('selected'); - } - } else { - $selectedLine.addClass('selected'); - } - } else if (event.ctrlKey) { - $selectedLine.toggleClass('selected'); - } else { - var $selected = $('.l.selected') - .not($selectedLine) - .removeClass('selected'); - if ($selected.length > 0) { - $selectedLine.addClass('selected'); - } else { - $selectedLine.toggleClass('selected'); - } - } - - lastLine = $selectedLine.hasClass('selected') ? selectedLine : null; - - // Update hash - var lines = $('.l.selected') - .map(function() { - return parseInt($(this).attr('id')); - }) - .get() - .sort(function(a, b) { - return a - b; - }); - - var hash = []; - var list = []; - for (var j = 0; j < lines.length; j++) { - if (0 === j && j + 1 === lines.length) { - hash.push(lines[j]); - } else if (0 === j) { - list[0] = lines[j]; - } else if (lines[j - 1] + 1 !== lines[j] && j + 1 === lines.length) { - hash.push(list.join('-')); - hash.push(lines[j]); - } else if (lines[j - 1] + 1 !== lines[j]) { - hash.push(list.join('-')); - list = [lines[j]]; - } else if (j + 1 === lines.length) { - list[1] = lines[j]; - hash.push(list.join('-')); - } else { - list[1] = lines[j]; - } - } - - window.location.hash = hash.join(','); - }); -}); - diff --git a/apigen/templates/woodocs/resources/style.css b/apigen/templates/woodocs/resources/style.css deleted file mode 100644 index 2aecb5cbb97..00000000000 --- a/apigen/templates/woodocs/resources/style.css +++ /dev/null @@ -1,488 +0,0 @@ -/*! - * ApiGen 2.7.0 - API documentation generator for PHP 5.3+ - * - * Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) - * Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) - * Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) - * Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - * - * For the full copyright and license information, please view - * the file LICENSE.md that was distributed with this source code. - */ - -body { - padding: 40px 0 0 0; -} - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -h2 { - font-size: 1.5em; - margin: 0.83em 0; -} - -h3 { - font-size: 1.17em; - margin: 1em 0 0.2em 0; -} - -h4 { - font-size: 100%; - margin: 0; - padding: 0; -} - -ul, ol { - margin-bottom: 0; -} - -a, a:hover { - text-decoration: none; -} - -var { - font-weight: bold; - font-style: normal; - color: #c09853; -} - -code { - color: #000000; - white-space: nowrap; -} - -code:empty { - display: none; -} - -code code { - border: none; - padding: 0; -} - -code a b { - color: #000000; -} - -pre code { - white-space: pre; -} - -.deprecated { - text-decoration: line-through; -} - -.invalid { - color: #dd1144; -} - -.hidden { - display: none; -} - -/* Left side */ -#left { - overflow: auto; - width: 270px; - height: 100%; - position: fixed; -} - -/* Menu */ -#menu { - padding: 10px; -} - -#menu h3 { - border-bottom: 1px solid #cccccc; - margin-left: -10px; - margin-right: -10px; - padding: 0 10px; -} - -#menu ul { - list-style: none; - padding: 0; - margin: 0; -} - -#menu ul ul { - padding-left: 10px; -} - -#menu li { - white-space: nowrap; - position: relative; -} - -#menu a { - display: block; - padding: 3px; - border-radius: 3px; -} - -#menu a:hover { - background-color: #a4698e; - color: #ffffff !important; -} - -#menu .active > a { - font-weight: bold; - color: #000000; -} - -#menu .active > a.invalid { - color: #dd1144; -} - -#menu #groups span { - position: absolute; - top: 6px; - right: 3px; - cursor: pointer; - display: block; - width: 12px; - height: 12px; - background: url('collapsed.png') transparent 0 0 no-repeat; -} - -#menu #groups span:hover { - background-position: -12px 0; -} - -#menu #groups span.collapsed { - background-position: 0 -12px; -} - -#menu #groups span.collapsed:hover { - background-position: -12px -12px; -} - -#menu #groups ul.collapsed { - display: none; -} - -/* Search */ -#menu .form-search { - margin-top: 0; -} - -#menu .search-query { - height: auto; - width: 100%; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} - -/* Autocomplete */ -.ac_results { - border-radius: 4px; - margin-top: 2px; - background-clip: padding-box; - background-color: #ffffff; - border-color: rgba(0, 0, 0, 0.2); - border-style: solid; - border-width: 1px; - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - overflow: hidden; - z-index: 99999; -} - -.ac_results ul { - width: 100%; - list-style-position: outside; - list-style: none; - padding: 0; - margin: 0; -} - -.ac_results li { - margin: 0; - padding: 2px 5px; - cursor: default; - display: block; - overflow: hidden; - white-space: nowrap; -} - -.ac_results li strong { - color: #000000; -} - -.ac_over { - background-color: #a4698e; - color: #ffffff; -} - -.ac_results li.ac_over strong { - color: #ffffff; -} - - -/* Right side */ -#right { - overflow: auto; - margin-left: 275px; - height: 100%; - position: fixed; - left: 0; - right: 0; -} - -#rightInner { - max-width: 1000px; - min-width: 350px; -} - -/* Navigation */ -.navbar-fixed-top .container { - width: auto; - padding: 0 1em; -} - -.navbar .nav > li > span { - display: block; - color: #999999; - line-height: 19px; - padding: 10px 10px 11px; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} - -.navbar .nav > li.active > span { - background-color: #222222; - color: #ffffff; -} - -/* Content */ -#content { - clear: both; - padding: 5px 15px; -} - -#content > .description { - margin: 1.2em 0; -} - -#content .alert-info { - margin-top: 18px; -} - -dl.tree { - margin: 1.2em 0; - padding: 10px; -} - -dl.tree dd { - margin: 0; - padding: 0; - line-height: 18px; -} - -.elementList { - line-height: 24px; -} - -h2.switchable { - background: transparent url('sort.png') no-repeat center right; - cursor: pointer; -} - -.summary td:first-child { - text-align: right; -} - -#packages.summary td:first-child, #namespaces.summary td:first-child, .inherited.summary td:first-child, .used.summary td:first-child { - text-align: left; -} - -.summary tr:hover td { - background: #f6f6f4; -} - -.summary .description p { - margin: 0; -} - -.class #methods.summary .description p:first-child, .summary .description.detailed h4:first-child { - margin-top: 8px; -} - -.summary .description p + p, .summary .description ul, .summary .description pre, .summary .description.detailed h4 { - margin-top: 4px; -} - -.summary dl { - margin: 0; -} - -.summary dd { - margin: 0 0 0 25px; -} - -.summary dt, dd { - line-height: 24px; -} - -.name, .attributes { - white-space: nowrap; -} - -.value { - white-space: pre-wrap; -} - -td.name, td.attributes { - width: 1%; -} - -.class #methods .name { - width: auto; - white-space: normal; -} - -.class #methods .name > div > code { - white-space: pre-wrap; -} - -.class #methods .name > div > code span, .function .value > code { - white-space: nowrap; -} - -.class #methods td.name > div, .class td.description > div { - position: relative; - padding-right: 1em; -} - -.attributes code, .name code, dd code { - color: #468847; -} - -.anchor { - position: absolute; - top: 0; - right: 0; - line-height: 1; - font-size: 85%; - margin: 0; - color: #a4698e !important; -} - -.list { - margin: 0 0 5px 25px; - line-height: 24px; -} - -/* Splitter */ -#splitter { - position: fixed; - height: 100%; - width: 5px; - left: 270px; - background: #a4698e url('resize.png') left center no-repeat; - cursor: e-resize; -} - -#splitter.active { - opacity: .5; -} - -/* Footer */ -#footer { - border-top: 1px solid #e5e5e5; - clear: both; - color: #808080; - text-align: right; - padding: 2em 1em; - margin: 3em 0 40px 0; -} - -/* Tree */ -div.tree ul { - list-style: none; - background: url('tree-vertical.png') left repeat-y; - padding: 0; - margin-left: 20px; -} - -div.tree li { - margin: 0; - padding: 0; -} - -div.tree div { - padding-left: 30px; -} - -div.tree div.notlast { - background: url('tree-hasnext.png') left 10px no-repeat; -} - -div.tree div.last { - background: url('tree-last.png') left -240px no-repeat; -} - -div.tree li.last { - background: url('tree-cleaner.png') left center repeat-y; -} - -div.tree span.padding { - padding-left: 15px; -} - -/* Source code */ -#source { - margin: 1em 0 1em 1em; - padding: 0; -} - -.php-keyword1 { - color: #468847; - font-weight: bold; -} - -.php-keyword2 { - font-weight: bold; -} - -.php-var { - color: #c09853; - font-weight: bold; -} - -.php-num { - color: #006dcc; -} - -.php-quote { - color: #006dcc; -} - -.php-comment { - color: #929292; -} - -.xlang { - color: #468847; - font-weight: bold; -} - -.l { - background: #fbfbfc; - margin-right: 8px; - padding: 2px 2px 2px 8px; - color: #c0c0c0; -} - -.l:target { - background: #a4698e url('code-line.png') right center no-repeat; - color: #ffffff; -} - -/* Small screens */ -#rightInner.medium .name, #rightInner.medium .attributes { - white-space: normal; -} diff --git a/apigen/templates/woodocs/robots.txt.latte b/apigen/templates/woodocs/robots.txt.latte deleted file mode 100644 index f895cd3a9f3..00000000000 --- a/apigen/templates/woodocs/robots.txt.latte +++ /dev/null @@ -1,14 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -User-agent: * -Disallow: -Sitemap: {$config->baseUrl}/sitemap.xml diff --git a/apigen/templates/woodocs/sitemap.xml.latte b/apigen/templates/woodocs/sitemap.xml.latte deleted file mode 100644 index 7a9027626ea..00000000000 --- a/apigen/templates/woodocs/sitemap.xml.latte +++ /dev/null @@ -1,36 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} - - - - {$config->baseUrl}/index.html - - - {$config->baseUrl}/{$namespace|namespaceUrl} - - - {$config->baseUrl}/{$package|packageUrl} - - -{define #elements} - - {$config->baseUrl}/{$element|elementUrl} - -{/define} - -{include #elements, elements => $classes} -{include #elements, elements => $interfaces} -{include #elements, elements => $traits} -{include #elements, elements => $exceptions} -{include #elements, elements => $constants} -{include #elements, elements => $functions} - diff --git a/apigen/templates/woodocs/source.latte b/apigen/templates/woodocs/source.latte deleted file mode 100644 index c803e3c48d1..00000000000 --- a/apigen/templates/woodocs/source.latte +++ /dev/null @@ -1,19 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $robots = false} - -{block #title}File {$fileName}{/block} - -{block #content} -
{!$source|sourceAnchors|replaceRE:'~(\s*)(\d+):(\s*)([^\\n]*\\n)?~','$1$2$3$4'}
-{/block} \ No newline at end of file diff --git a/apigen/templates/woodocs/todo.latte b/apigen/templates/woodocs/todo.latte deleted file mode 100644 index 96ac9b1bbf2..00000000000 --- a/apigen/templates/woodocs/todo.latte +++ /dev/null @@ -1,129 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'todo'} - -{block #title}Todo{/block} - -{block #content} -
-

{include #title}

- - {define #classes} - {foreach $items as $class} -
{$class->name}{!$description|annotation:'todo':$class}
- {include #classes, items => $todoClasses} -
- {/if} - - {if $todoInterfaces} -

Interfaces summary

- - {include #classes, items => $todoInterfaces} -
- {/if} - - {if $todoTraits} -

Traits summary

- - {include #classes, items => $todoTraits} -
- {/if} - - {if $todoExceptions} -

Exceptions summary

- - {include #classes, items => $todoExceptions} -
- {/if} - - {if $todoMethods} -

Methods summary

- - {foreach $todoMethods as $method} - - {var $count = count($method->annotations['todo'])} - - - {foreach $method->annotations['todo'] as $description} - {sep}{/sep} - {/foreach} - - {/foreach} -
{$method->declaringClassName}{$method->name}(){!$description|annotation:'todo':$method}
- {/if} - - {if $todoConstants} -

Constants summary

- - {foreach $todoConstants as $constant} - - {var $count = count($constant->annotations['todo'])} - {if $constant->declaringClassName} - - - {else} - - - {/if} - {foreach $constant->annotations['todo'] as $description} - {sep}{/sep} - {/foreach} - - {/foreach} -
{$constant->declaringClassName}{$constant->name}{$constant->namespaceName}{$constant->shortName}{!$description|annotation:'todo':$constant}
- {/if} - - {if $todoProperties} -

Properties summary

- - {foreach $todoProperties as $property} - - {var $count = count($property->annotations['todo'])} - - - {foreach $property->annotations['todo'] as $description} - {sep}{/sep} - {/foreach} - - {/foreach} -
{$property->declaringClassName}${$property->name}{!$description|annotation:'todo':$property}
- {/if} - - {if $todoFunctions} -

Functions summary

- - {foreach $todoFunctions as $function} - - {var $count = count($function->annotations['todo'])} - - - {foreach $function->annotations['todo'] as $description} - {sep}{/sep} - {/foreach} - - {/foreach} -
{$function->namespaceName}{$function->shortName}{!$description|annotation:'todo':$function}
- {/if} -
-{/block} \ No newline at end of file diff --git a/apigen/templates/woodocs/tree.latte b/apigen/templates/woodocs/tree.latte deleted file mode 100644 index adbfda8c2ad..00000000000 --- a/apigen/templates/woodocs/tree.latte +++ /dev/null @@ -1,73 +0,0 @@ -{* -ApiGen 2.8.0 - API documentation generator for PHP 5.3+ - -Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com) -Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich) -Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville) -Copyright (c) 2012 Olivier Laviale (https://github.com/olvlvl) - -For the full copyright and license information, please view -the file LICENSE.md that was distributed with this source code. -*} -{layout '@layout.latte'} -{var $active = 'tree'} - -{block #title}Tree{/block} - -{define #tree} -
-
    - {var $level = -1} - {? foreach ($tree as $reflectionName => $reflection): } - {if $level === $tree->getDepth()} - - {elseif $level > $tree->getDepth()} - {!'
'|repeat:$level - $tree->getDepth()} - {elseif -1 !== $level} -
    - {/if} - -
  • {$reflectionName} - {var $interfaces = $reflection->ownInterfaces} - {if $interfaces} implements {foreach $interfaces as $interface} - {$interface->name}{sep}, {/sep} - {/foreach}{/if} - {var $traits = $reflection->ownTraits} - {if $traits}{if $interfaces}
    {/if} uses {foreach $traits as $trait} - {$trait->name}{sep}, {/sep} - {/foreach}{/if} -
    - - {var $level = $tree->getDepth()} - {? endforeach; } -
  • - {!'
'|repeat:$level} - -
-{/define} - -{block #content} -
-

{include #title}

- - {if $classTree->valid()} -

Classes

- {include #tree, tree => $classTree} - {/if} - - {if $interfaceTree->valid()} -

Interfaces

- {include #tree, tree => $interfaceTree} - {/if} - - {if $traitTree->valid()} -

Traits

- {include #tree, tree => $traitTree} - {/if} - - {if $exceptionTree->valid()} -

Exceptions

- {include #tree, tree => $exceptionTree} - {/if} -
-{/block} diff --git a/apigen/theme-woocommerce/404.latte b/apigen/theme-woocommerce/404.latte new file mode 100644 index 00000000000..58e538f1a01 --- /dev/null +++ b/apigen/theme-woocommerce/404.latte @@ -0,0 +1,13 @@ +{layout '@layout.latte'} +{var $robots = false} + +{block title}Page not found{/block} + +{block content} +
+

{include title}

+

The requested page could not be found.

+

You have probably clicked on a link that is outdated and points to a page that does not exist any more or you have made an typing error in the address.

+

To continue please try to find requested page in the menu,{if $config->tree} take a look at the tree view of the whole project{/if} or use search field on the top.

+
+{/block} diff --git a/apigen/theme-woocommerce/@elementlist.latte b/apigen/theme-woocommerce/@elementlist.latte new file mode 100644 index 00000000000..3c8b2f3e52a --- /dev/null +++ b/apigen/theme-woocommerce/@elementlist.latte @@ -0,0 +1,60 @@ +{define elements} + + {if $namespace}{$element->shortName}{else}{$element->name}{/if} + {$element|shortDescription|noescape} + +{/define} + +{if $classes} +
+

Classes summary

+ + {include elements, elements => $classes} +
+
+{/if} + +{if $interfaces} +
+

Interfaces summary

+ + {include elements, elements => $interfaces} +
+
+{/if} + +{if $traits} +
+

Traits summary

+ + {include elements, elements => $traits} +
+
+{/if} + +{if $exceptions} +
+

Exceptions summary

+ + {include elements, elements => $exceptions} +
+
+{/if} + +{if $constants} +
+

Constants summary

+ + {include elements, elements => $constants} +
+
+{/if} + +{if $functions} +
+

Functions summary

+ + {include elements, elements => $functions} +
+
+{/if} diff --git a/apigen/theme-woocommerce/@layout.latte b/apigen/theme-woocommerce/@layout.latte new file mode 100644 index 00000000000..136673b87b4 --- /dev/null +++ b/apigen/theme-woocommerce/@layout.latte @@ -0,0 +1,185 @@ +{default $robots = true} +{default $active = ''} + + + + + + + {include title}{if 'overview' !== $active && $config->title} | {$config->title}{/if} + + + + + + + + + + + +
+ +
+ +
+ + + + + + diff --git a/apigen/theme-woocommerce/annotation-group.latte b/apigen/theme-woocommerce/annotation-group.latte new file mode 100644 index 00000000000..5bac96b6d75 --- /dev/null +++ b/apigen/theme-woocommerce/annotation-group.latte @@ -0,0 +1,149 @@ +{layout '@layout.latte'} +{var $active = 'annotation-group-' . $annotation} + +{block title}{$annotation|firstUpper}{/block} + +{block content} +
+

{include title}

+ + {if $hasElements} + {if $annotationClasses} +
+

Classes summary

+ + {include classes, items => $annotationClasses} +
+
+ {/if} + + {if $annotationInterfaces} +
+

Interfaces summary

+ + {include classes, items => $annotationInterfaces} +
+
+ {/if} + + {if $annotationTraits} +
+

Traits summary

+ + {include classes, items => $annotationTraits} +
+
+ {/if} + + {if $annotationExceptions} +
+

Exceptions summary

+ + {include classes, items => $annotationExceptions} +
+
+ {/if} + + {if $annotationMethods} +
+

Methods summary

+ + + + + + +
{$method->declaringClassName}{$method->name}() + {if $method->hasAnnotation($annotation)} + {foreach $method->annotations[$annotation] as $description} + {if $description} + {$description|annotation:$annotation:$method|noescape}
+ {/if} + {/foreach} + {/if} +
+
+ {/if} + + {if $annotationConstants} +
+

Constants summary

+ + + {if $constant->declaringClassName} + + + + {else} + + + {/if} + + +
{$constant->declaringClassName}{$constant->name}{$constant->namespaceName}{$constant->shortName} + {foreach $constant->annotations[$annotation] as $description} + {if $description} + {$description|annotation:$annotation:$constant|noescape}
+ {/if} + {/foreach} +
+
+ {/if} + + {if $annotationProperties} +
+

Properties summary

+ + + + + + +
{$property->declaringClassName}${$property->name} + {foreach $property->annotations[$annotation] as $description} + {if $description} + {$description|annotation:$annotation:$property|noescape}
+ {/if} + {/foreach} +
+
+ {/if} + + {if $annotationFunctions} +
+

Functions summary

+ + + + + + +
{$function->namespaceName}{$function->shortName} + {foreach $function->annotations[$annotation] as $description} + {if $description} + {$description|annotation:$annotation:$function|noescape}
+ {/if} + {/foreach} +
+
+ {/if} + + {else} +

No elements with @{$annotation} annotation found.

+ {/if} +
+{/block} + + +{define classes} + + {$class->name} + + {foreach $class->annotations[$annotation] as $description} + {if $description} + {$description|annotation:$annotation:$class|noescape}
+ {/if} + {/foreach} + + +{/define} diff --git a/apigen/theme-woocommerce/class.latte b/apigen/theme-woocommerce/class.latte new file mode 100644 index 00000000000..09435e937dc --- /dev/null +++ b/apigen/theme-woocommerce/class.latte @@ -0,0 +1,462 @@ +{layout '@layout.latte'} +{var $active = 'class'} + +{block title}{if $class->deprecated}Deprecated {/if}{if $class->interface}Interface{elseif $class->trait}Trait{else}Class{/if} {$class->name}{/block} + +{block content} +
+

{if $class->interface}Interface{elseif $class->trait}Trait{else}Class{/if} {$class->shortName}

+ + {if $class->valid} + +
+ {$class|longDescription|noescape} +
+ +
+
+ Extended by + {if $item->documented} + {last}{/last}{$item->name}{last}{/last} + {else}{$item->name}{/if} + {var $itemOwnInterfaces = $item->ownInterfaces} + {if $itemOwnInterfaces} implements {foreach $itemOwnInterfaces as $interface} + {$interface->name}{sep}, {/sep} + {/foreach}{/if} + {var $itemOwnTraits = $item->ownTraits} + {if $itemOwnTraits} uses {foreach $itemOwnTraits as $trait} + {if is_string($trait)} + {$trait} (not available) + + {else} + {$trait->name}{sep}, {/sep} + {/} + {/foreach}{/if} +
+
+ + {define children} +

+ {foreach $children as $child} + {$child->name}{sep}, {/sep} + {/foreach} +

+ {/define} + +
+

Direct known subclasses

+ {include children, children => $directSubClasses} +
+ +
+

Indirect known subclasses

+ {include children, children => $indirectSubClasses} +
+ +
+

Direct known implementers

+ {include children, children => $directImplementers} +
+ +
+

Indirect known implementers

+ {include children, children => $indirectImplementers} +
+ +
+

Direct Known Users

+ {include children, children => $directUsers} +
+ +
+

Indirect Known Users

+ {include children, children => $indirectUsers} +
+ +
+ {if !$class->interface && !$class->trait && ($class->abstract || $class->final)}{if $class->abstract}Abstract{else}Final{/if}
{/if} + {if $class->internal}PHP Extension: {$class->extension->name|firstUpper}
{/if} + {if $class->inNamespace()}Namespace: {$class->namespaceName|namespaceLinks|noescape}
{/if} + {if $class->inPackage()}Package: {$class->packageName|packageLinks|noescape}
{/if} + + {foreach $template->annotationSort($template->annotationFilter($class->annotations)) as $annotation => $values} + {foreach $values as $value} + {$annotation|annotationBeautify}{if $value}:{/if} + {$value|annotation:$annotation:$class|noescape}
+ {/foreach} + {/foreach} + {if $class->internal} + Documented at php.net + {else} + Located at {$class->fileName|relativePath} + {/if} +
+
+ + {var $ownMethods = $class->ownMethods} + {var $inheritedMethods = $class->inheritedMethods} + {var $usedMethods = $class->usedMethods} + {var $ownMagicMethods = $class->ownMagicMethods} + {var $inheritedMagicMethods = $class->inheritedMagicMethods} + {var $usedMagicMethods = $class->usedMagicMethods} + + {if $ownMethods || $inheritedMethods || $usedMethods || $ownMagicMethods || $usedMagicMethods} + {define method} + + {var $annotations = $method->annotations} + + + {if !$class->interface && $method->abstract}abstract{elseif $method->final}final{/if} {if $method->protected}protected{elseif $method->private}private{else}public{/if} {if $method->static}static{/if} + {ifset $annotations['return']}{$annotations['return'][0]|typeLinks:$method|noescape}{/ifset} + {if $method->returnsReference()}&{/if} + + + +
+ # + {block|strip} + {if $class->internal} + {$method->name}( + {else} + {$method->name}( + {/if} + {foreach $method->parameters as $parameter} + {$parameter->typeHint|typeLinks:$method|noescape} + {if $parameter->passedByReference}& {/if}${$parameter->name}{if $parameter->defaultValueAvailable} = {$parameter->defaultValueDefinition|highlightPHP:$class|noescape}{elseif $parameter->unlimited},…{/if}{sep}, {/sep} + {/foreach} + ){/block} + + {if $config->template['options']['elementDetailsCollapsed']} +
+ {$method|shortDescription:true|noescape} +
+ {/if} + +
+ {$method|longDescription|noescape} + + {if !$class->deprecated && $method->deprecated} +

Deprecated

+ {ifset $annotations['deprecated']} +
+ {foreach $annotations['deprecated'] as $description} + {if $description} + {$description|annotation:'deprecated':$method|noescape}
+ {/if} + {/foreach} +
+ {/ifset} + {/if} + + {if $method->parameters && isset($annotations['param'])} +

Parameters

+
+ {foreach $method->parameters as $parameter} +
${$parameter->name}{if $parameter->unlimited},…{/if}
+
{$parameter->description|description:$method|noescape}
+ {/foreach} +
+ {/if} + + {if isset($annotations['return']) && 'void' !== $annotations['return'][0]} +

Returns

+
+ {foreach $annotations['return'] as $description} + {$description|annotation:'return':$method|noescape}{sep}
{/} + {/foreach} +
+ {/if} + + {ifset $annotations['throws']} +

Throws

+
+ {foreach $annotations['throws'] as $description} + {$description|annotation:'throws':$method|noescape}{sep}
{/} + {/foreach} +
+ {/ifset} + + {foreach $template->annotationSort($template->annotationFilter($annotations, array('deprecated', 'param', 'return', 'throws'))) as $annotation => $descriptions} +

{$annotation|annotationBeautify}

+
+ {foreach $descriptions as $description} + {if $description} + {$description|annotation:$annotation:$method|noescape}
+ {/if} + {/foreach} +
+ {/foreach} + + {var $overriddenMethod = $method->overriddenMethod} + {if $overriddenMethod} +

Overrides

+ + {/if} + + {var $implementedMethod = $method->implementedMethod} + {if $implementedMethod} +

Implementation of

+ + {/if} +
+
+ + {/define} + +
+

Methods summary

+ + {foreach $ownMethods as $method} + {include method, method => $method} + {/foreach} +
+
+ + {foreach $inheritedMethods as $parentName => $methods} +
+

Methods inherited from {$parentName}

+

+ {foreach $methods as $method} + {$method->name}(){sep}, {/sep} + {/foreach} +

+
+ {/foreach} + + {foreach $usedMethods as $traitName => $methods} +
+

Methods used from {$traitName}

+

+ {foreach $methods as $data} + {$data['method']->name}(){if $data['aliases']}(as {foreach $data['aliases'] as $alias}{$alias->name}(){sep}, {/sep}{/foreach}){/if}{sep}, {/sep} + {/foreach} +

+
+ {/foreach} + + {if $ownMagicMethods} +
+

Magic methods summary

+ + {foreach $ownMagicMethods as $method} + {include method, method => $method} + {/foreach} +
+
+ {/if} + + {foreach $inheritedMagicMethods as $parentName => $methods} +
+

Magic methods inherited from {$parentName}

+

+ {foreach $methods as $method} + {$method->name}(){sep}, {/sep} + {/foreach} +

+
+ {/foreach} + + {foreach $usedMagicMethods as $traitName => $methods} +
+

Magic methods used from {$traitName}

+

+ {foreach $methods as $data} + {$data['method']->name}(){if $data['aliases']}(as {foreach $data['aliases'] as $alias}{$alias->name}(){sep}, {/sep}{/foreach}){/if}{sep}, {/sep} + {/foreach} +

+
+ {/foreach} + {/if} + + + {var $ownConstants = $class->ownConstants} + {var $inheritedConstants = $class->inheritedConstants} + + {if $ownConstants || $inheritedConstants} +
+

Constants summary

+ + + {var $annotations = $constant->annotations} + + + + + +
{$constant->typeHint|typeLinks:$constant|noescape} + + {if $class->internal} + {$constant->name} + {else} + {$constant->name} + {/if} + + +
+ {$constant|shortDescription:true|noescape} +
+ +
+ {$constant|longDescription|noescape} + + {foreach $template->annotationSort($template->annotationFilter($annotations, array('var'))) as $annotation => $descriptions} +

{$annotation|annotationBeautify}

+
+ {foreach $descriptions as $description} + {if $description} + {$description|annotation:$annotation:$constant|noescape}
+ {/if} + {/foreach} +
+ {/foreach} +
+
+
+ # + {$constant->valueDefinition|highlightValue:$class|noescape} +
+
+
+ + {foreach $inheritedConstants as $parentName => $constants} +
+

Constants inherited from {$parentName}

+

+ {foreach $constants as $constant} + {$constant->name}{sep}, {/sep} + {/foreach} +

+
+ {/foreach} + {/if} + + {var $ownProperties = $class->ownProperties} + {var $inheritedProperties = $class->inheritedProperties} + {var $usedProperties = $class->usedProperties} + {var $ownMagicProperties = $class->ownMagicProperties} + {var $inheritedMagicProperties = $class->inheritedMagicProperties} + {var $usedMagicProperties = $class->usedMagicProperties} + + {if $ownProperties || $inheritedProperties || $usedProperties || $ownMagicProperties || $inheritedMagicProperties || $usedMagicProperties} + {define property} + + + {if $property->protected}protected{elseif $property->private}private{else}public{/if} {if $property->static}static{/if} {if $property->readOnly}read-only{elseif $property->writeOnly}write-only{/if} + {$property->typeHint|typeLinks:$property|noescape} + + + + {if $class->internal} + ${$property->name} + {else} + ${$property->name} + {/if} + +
+ {$property|shortDescription:true|noescape} +
+ +
+ {$property|longDescription|noescape} + + {foreach $template->annotationSort($template->annotationFilter($property->annotations, array('var'))) as $annotation => $descriptions} +

{$annotation|annotationBeautify}

+
+ {foreach $descriptions as $description} + {if $description} + {$description|annotation:$annotation:$property|noescape}
+ {/if} + {/foreach} +
+ {/foreach} +
+ + +
+ # + {$property->defaultValueDefinition|highlightValue:$class|noescape} +
+ + + {/define} + +
+

Properties summary

+ + {foreach $ownProperties as $property} + {include property, property => $property} + {/foreach} +
+
+ + {foreach $inheritedProperties as $parentName => $properties} +
+

Properties inherited from {$parentName}

+

+ {foreach $properties as $property} + ${$property->name}{sep}, {/sep} + {/foreach} +

+
+ {/foreach} + + {foreach $usedProperties as $traitName => $properties} +
+

Properties used from {$traitName}

+

+ {foreach $properties as $property} + ${$property->name}{sep}, {/sep} + {/foreach} +

+
+ {/foreach} + + {if $ownMagicProperties} +
+

Magic properties

+ + {foreach $ownMagicProperties as $property} + {include property, property => $property} + {/foreach} +
+
+ {/if} + + {foreach $inheritedMagicProperties as $parentName => $properties} +
+

Magic properties inherited from {$parentName}

+

+ {foreach $properties as $property} + ${$property->name}{sep}, {/sep} + {/foreach} +

+
+ {/foreach} + + {foreach $usedMagicProperties as $traitName => $properties} +
+

Magic properties used from {$traitName}

+

+ {foreach $properties as $property} + ${$property->name}{sep}, {/sep} + {/foreach} +

+
+ {/foreach} + {/if} + + {else} +
+

+ Documentation of this class could not be generated. +

+

+ Class was originally declared in {$class->fileName|relativePath} and is invalid because of: +

+
    +
  • Class was redeclared in {$reason->getSender()->getFileName()|relativePath}.
  • +
+
+ {/if} +
+{/block} diff --git a/apigen/theme-woocommerce/combined.js.latte b/apigen/theme-woocommerce/combined.js.latte new file mode 100644 index 00000000000..93ba3e4d75b --- /dev/null +++ b/apigen/theme-woocommerce/combined.js.latte @@ -0,0 +1,10 @@ +{contentType javascript} + +var ApiGen = ApiGen || {}; +ApiGen.config = {$config->template}; + +{var $scripts = ['jquery.min.js', 'jquery.cookie.js', 'jquery.sprintf.js', 'jquery.autocomplete.js', 'jquery.sortElements.js', 'main.js']} + +{foreach $scripts as $script} + {file_get_contents("$basePath/js/$script")|noescape} +{/foreach} diff --git a/apigen/theme-woocommerce/config.neon b/apigen/theme-woocommerce/config.neon new file mode 100644 index 00000000000..a5d77eeb65c --- /dev/null +++ b/apigen/theme-woocommerce/config.neon @@ -0,0 +1 @@ +name: "Twitter Bootstrap theme" diff --git a/apigen/theme-woocommerce/constant.latte b/apigen/theme-woocommerce/constant.latte new file mode 100644 index 00000000000..ec561819357 --- /dev/null +++ b/apigen/theme-woocommerce/constant.latte @@ -0,0 +1,60 @@ +{layout '@layout.latte'} +{var $active = 'constant'} + +{block title}{if $constant->deprecated}Deprecated {/if}Constant {$constant->name}{/block} + +{block content} +
+

Constant {$constant->shortName}

+ + {if $constant->valid} + +
+ {$constant|longDescription|noescape} +
+ +
+ {if $constant->inNamespace()}Namespace: {$constant->namespaceName|namespaceLinks|noescape}
{/if} + {if $constant->inPackage()}Package: {$constant->packageName|packageLinks|noescape}
{/if} + {foreach $template->annotationSort($template->annotationFilter($constant->annotations, array('var'))) as $annotation => $values} + {foreach $values as $value} + {$annotation|annotationBeautify}{if $value}:{/if} + {$value|annotation:$annotation:$constant|noescape}
+ {/foreach} + {/foreach} + Located at + + {$constant->fileName|relativePath} +
+
+ + {var $annotations = $constant->annotations} + +
+

Value summary

+ + + + + +
{$constant->typeHint|typeLinks:$constant|noescape}{block|strip} + {var $element = $template->resolveElement($constant->valueDefinition, $constant)} + {if $element}{$constant->valueDefinition}{else}{$constant->valueDefinition|highlightValue:$constant|noescape}{/if} + {/block}
+
+ + {else} +
+

+ Documentation of this constant could not be generated. +

+

+ Constant was originally declared in {$constant->fileName|relativePath} and is invalid because of: +

+
    +
  • Constant was redeclared in {$reason->getSender()->getFileName()|relativePath}.
  • +
+
+ {/if} +
+{/block} diff --git a/apigen/theme-woocommerce/elementlist.js.latte b/apigen/theme-woocommerce/elementlist.js.latte new file mode 100644 index 00000000000..8becc227964 --- /dev/null +++ b/apigen/theme-woocommerce/elementlist.js.latte @@ -0,0 +1,4 @@ +{contentType javascript} + +var ApiGen = ApiGen || {}; +ApiGen.elements = {$elements}; diff --git a/apigen/theme-woocommerce/function.latte b/apigen/theme-woocommerce/function.latte new file mode 100644 index 00000000000..78849ae5988 --- /dev/null +++ b/apigen/theme-woocommerce/function.latte @@ -0,0 +1,94 @@ +{layout '@layout.latte'} +{var $active = 'function'} + +{block title}{if $function->deprecated}Deprecated {/if}Function {$function->name}{/block} + +{block content} +
+

Function {$function->shortName}

+ + {if $function->valid} + +
+ {$function|longDescription|noescape} +
+ +
+ {if $function->inNamespace()}Namespace: {$function->namespaceName|namespaceLinks|noescape}
{/if} + {if $function->inPackage()}Package: {$function->packageName|packageLinks|noescape}
{/if} + {foreach $template->annotationSort($template->annotationFilter($function->annotations, array('param', 'return', 'throws'))) as $annotation => $values} + {foreach $values as $value} + {$annotation|annotationBeautify}{if $value}:{/if} + {$value|annotation:$annotation:$function|noescape}
+ {/foreach} + {/foreach} + Located at + + {$function->fileName|relativePath} +
+
+ + {var $annotations = $function->annotations} + + {if count($function->parameters)} +
+

Parameters summary

+ + + + + + +
{$parameter->typeHint|typeLinks:$function|noescape}{block|strip} + {if $parameter->passedByReference}& {/if}${$parameter->name}{if $parameter->defaultValueAvailable} = {$parameter->defaultValueDefinition|highlightPHP:$function|noescape}{elseif $parameter->unlimited},…{/if} + {/block}{$parameter->description|description:$function}
+
+ {/if} + + {if isset($annotations['return']) && 'void' !== $annotations['return'][0]} +
+

Return value summary

+ + + + + +
+ {$annotations['return'][0]|typeLinks:$function|noescape} + + {$annotations['return'][0]|description:$function|noescape} +
+
+ {/if} + + {if isset($annotations['throws'])} +
+

Thrown exceptions summary

+ + + + + +
+ {$throws|typeLinks:$function|noescape} + + {$throws|description:$function|noescape} +
+
+ {/if} + + {else} +
+

+ Documentation of this function could not be generated. +

+

+ Function was originally declared in {$function->fileName|relativePath} and is invalid because of: +

+
    +
  • Function was redeclared in {$reason->getSender()->getFileName()|relativePath}.
  • +
+
+ {/if} +
+{/block} diff --git a/apigen/theme-woocommerce/js/jquery.autocomplete.js b/apigen/theme-woocommerce/js/jquery.autocomplete.js new file mode 100644 index 00000000000..0923c462ab9 --- /dev/null +++ b/apigen/theme-woocommerce/js/jquery.autocomplete.js @@ -0,0 +1,841 @@ +/* + * jQuery Autocomplete plugin 1.2.3 + * + * Copyright (c) 2009 Jörn Zaefferer + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * With small modifications by Alfonso Gómez-Arzola. + * See changelog for details. + * + */ + +;(function($) { + +$.fn.extend({ + autocomplete: function(urlOrData, options) { + var isUrl = typeof urlOrData == "string"; + options = $.extend({}, $.Autocompleter.defaults, { + url: isUrl ? urlOrData : null, + data: isUrl ? null : urlOrData, + delay: isUrl ? $.Autocompleter.defaults.delay : 10, + max: options && !options.scroll ? 10 : 150, + noRecord: "No Records." + }, options); + + // if highlight is set to false, replace it with a do-nothing function + options.highlight = options.highlight || function(value) { return value; }; + + // if the formatMatch option is not specified, then use formatItem for backwards compatibility + options.formatMatch = options.formatMatch || options.formatItem; + + return this.each(function() { + new $.Autocompleter(this, options); + }); + }, + result: function(handler) { + return this.bind("result", handler); + }, + search: function(handler) { + return this.trigger("search", [handler]); + }, + flushCache: function() { + return this.trigger("flushCache"); + }, + setOptions: function(options){ + return this.trigger("setOptions", [options]); + }, + unautocomplete: function() { + return this.trigger("unautocomplete"); + } +}); + +$.Autocompleter = function(input, options) { + + var KEY = { + UP: 38, + DOWN: 40, + DEL: 46, + TAB: 9, + RETURN: 13, + ESC: 27, + COMMA: 188, + PAGEUP: 33, + PAGEDOWN: 34, + BACKSPACE: 8 + }; + + var globalFailure = null; + if(options.failure != null && typeof options.failure == "function") { + globalFailure = options.failure; + } + + // Create $ object for input element + var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); + + var timeout; + var previousValue = ""; + var cache = $.Autocompleter.Cache(options); + var hasFocus = 0; + var lastKeyPressCode; + var config = { + mouseDownOnSelect: false + }; + var select = $.Autocompleter.Select(options, input, selectCurrent, config); + + var blockSubmit; + + // prevent form submit in opera when selecting with return key + navigator.userAgent.indexOf("Opera") != -1 && $(input.form).bind("submit.autocomplete", function() { + if (blockSubmit) { + blockSubmit = false; + return false; + } + }); + + // older versions of opera don't trigger keydown multiple times while pressed, others don't work with keypress at all + $input.bind((navigator.userAgent.indexOf("Opera") != -1 && !'KeyboardEvent' in window ? "keypress" : "keydown") + ".autocomplete", function(event) { + // a keypress means the input has focus + // avoids issue where input had focus before the autocomplete was applied + hasFocus = 1; + // track last key pressed + lastKeyPressCode = event.keyCode; + switch(event.keyCode) { + + case KEY.UP: + if ( select.visible() ) { + event.preventDefault(); + select.prev(); + } else { + onChange(0, true); + } + break; + + case KEY.DOWN: + if ( select.visible() ) { + event.preventDefault(); + select.next(); + } else { + onChange(0, true); + } + break; + + case KEY.PAGEUP: + if ( select.visible() ) { + event.preventDefault(); + select.pageUp(); + } else { + onChange(0, true); + } + break; + + case KEY.PAGEDOWN: + if ( select.visible() ) { + event.preventDefault(); + select.pageDown(); + } else { + onChange(0, true); + } + break; + + // matches also semicolon + case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: + case KEY.TAB: + case KEY.RETURN: + if( selectCurrent() ) { + // stop default to prevent a form submit, Opera needs special handling + event.preventDefault(); + blockSubmit = true; + return false; + } + break; + + case KEY.ESC: + select.hide(); + break; + + default: + clearTimeout(timeout); + timeout = setTimeout(onChange, options.delay); + break; + } + }).focus(function(){ + // track whether the field has focus, we shouldn't process any + // results if the field no longer has focus + hasFocus++; + }).blur(function() { + hasFocus = 0; + if (!config.mouseDownOnSelect) { + hideResults(); + } + }).click(function() { + // show select when clicking in a focused field + // but if clickFire is true, don't require field + // to be focused to begin with; just show select + if( options.clickFire ) { + if ( !select.visible() ) { + onChange(0, true); + } + } else { + if ( hasFocus++ > 1 && !select.visible() ) { + onChange(0, true); + } + } + }).bind("search", function() { + var fn = (arguments.length > 1) ? arguments[1] : null; + function findValueCallback(q, data) { + var result; + if( data && data.length ) { + for (var i=0; i < data.length; i++) { + if( data[i].result.toLowerCase() == q.toLowerCase() ) { + result = data[i]; + break; + } + } + } + if( typeof fn == "function" ) fn(result); + else $input.trigger("result", result && [result.data, result.value]); + } + $.each(trimWords($input.val()), function(i, value) { + request(value, findValueCallback, findValueCallback); + }); + }).bind("flushCache", function() { + cache.flush(); + }).bind("setOptions", function() { + $.extend(true, options, arguments[1]); + // if we've updated the data, repopulate + if ( "data" in arguments[1] ) + cache.populate(); + }).bind("unautocomplete", function() { + select.unbind(); + $input.unbind(); + $(input.form).unbind(".autocomplete"); + }); + + + function selectCurrent() { + var selected = select.selected(); + if( !selected ) + return false; + + var v = selected.result; + previousValue = v; + + if ( options.multiple ) { + var words = trimWords($input.val()); + if ( words.length > 1 ) { + var seperator = options.multipleSeparator.length; + var cursorAt = $(input).selection().start; + var wordAt, progress = 0; + $.each(words, function(i, word) { + progress += word.length; + if (cursorAt <= progress) { + wordAt = i; + return false; + } + progress += seperator; + }); + words[wordAt] = v; + //$.Autocompleter.Selection(input, progress + seperator, progress + seperator); + v = words.join( options.multipleSeparator ); + } + v += options.multipleSeparator; + } + + $input.val(v); + hideResultsNow(); + $input.trigger("result", [selected.data, selected.value]); + return true; + } + + function onChange(crap, skipPrevCheck) { + if( lastKeyPressCode == KEY.DEL ) { + select.hide(); + return; + } + + var currentValue = $input.val(); + + if ( !skipPrevCheck && currentValue == previousValue ) + return; + + previousValue = currentValue; + + currentValue = lastWord(currentValue); + if ( currentValue.length >= options.minChars) { + $input.addClass(options.loadingClass); + if (!options.matchCase) + currentValue = currentValue.toLowerCase(); + request(currentValue, receiveData, hideResultsNow); + } else { + stopLoading(); + select.hide(); + } + }; + + function trimWords(value) { + if (!value) + return [""]; + if (!options.multiple) + return [$.trim(value)]; + return $.map(value.split(options.multipleSeparator), function(word) { + return $.trim(value).length ? $.trim(word) : null; + }); + } + + function lastWord(value) { + if ( !options.multiple ) + return value; + var words = trimWords(value); + if (words.length == 1) + return words[0]; + var cursorAt = $(input).selection().start; + if (cursorAt == value.length) { + words = trimWords(value) + } else { + words = trimWords(value.replace(value.substring(cursorAt), "")); + } + return words[words.length - 1]; + } + + // fills in the input box w/the first match (assumed to be the best match) + // q: the term entered + // sValue: the first matching result + function autoFill(q, sValue){ + // autofill in the complete box w/the first match as long as the user hasn't entered in more data + // if the last user key pressed was backspace, don't autofill + if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) { + // fill in the value (keep the case the user has typed) + $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); + // select the portion of the value not typed by the user (so the next character will erase) + $(input).selection(previousValue.length, previousValue.length + sValue.length); + } + }; + + function hideResults() { + clearTimeout(timeout); + timeout = setTimeout(hideResultsNow, 200); + }; + + function hideResultsNow() { + var wasVisible = select.visible(); + select.hide(); + clearTimeout(timeout); + stopLoading(); + if (options.mustMatch) { + // call search and run callback + $input.search( + function (result){ + // if no value found, clear the input box + if( !result ) { + if (options.multiple) { + var words = trimWords($input.val()).slice(0, -1); + $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") ); + } + else { + $input.val( "" ); + $input.trigger("result", null); + } + } + } + ); + } + }; + + function receiveData(q, data) { + if ( data && data.length && hasFocus ) { + stopLoading(); + select.display(data, q); + autoFill(q, data[0].value); + select.show(); + } else { + hideResultsNow(); + } + }; + + function request(term, success, failure) { + if (!options.matchCase) + term = term.toLowerCase(); + var data = cache.load(term); + // recieve the cached data + if (data) { + if(data.length) { + success(term, data); + } + else{ + var parsed = options.parse && options.parse(options.noRecord) || parse(options.noRecord); + success(term,parsed); + } + // if an AJAX url has been supplied, try loading the data now + } else if( (typeof options.url == "string") && (options.url.length > 0) ){ + + var extraParams = { + timestamp: +new Date() + }; + $.each(options.extraParams, function(key, param) { + extraParams[key] = typeof param == "function" ? param() : param; + }); + + $.ajax({ + // try to leverage ajaxQueue plugin to abort previous requests + mode: "abort", + // limit abortion to this input + port: "autocomplete" + input.name, + dataType: options.dataType, + url: options.url, + data: $.extend({ + q: lastWord(term), + limit: options.max + }, extraParams), + success: function(data) { + var parsed = options.parse && options.parse(data) || parse(data); + cache.add(term, parsed); + success(term, parsed); + } + }); + } else { + // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match + select.emptyList(); + if(globalFailure != null) { + globalFailure(); + } + else { + failure(term); + } + } + }; + + function parse(data) { + var parsed = []; + var rows = data.split("\n"); + for (var i=0; i < rows.length; i++) { + var row = $.trim(rows[i]); + if (row) { + row = row.split("|"); + parsed[parsed.length] = { + data: row, + value: row[0], + result: options.formatResult && options.formatResult(row, row[0]) || row[0] + }; + } + } + return parsed; + }; + + function stopLoading() { + $input.removeClass(options.loadingClass); + }; + +}; + +$.Autocompleter.defaults = { + inputClass: "ac_input", + resultsClass: "ac_results", + loadingClass: "ac_loading", + minChars: 1, + delay: 400, + matchCase: false, + matchSubset: true, + matchContains: false, + cacheLength: 100, + max: 1000, + mustMatch: false, + extraParams: {}, + selectFirst: true, + formatItem: function(row) { return row[0]; }, + formatMatch: null, + autoFill: false, + width: 0, + multiple: false, + multipleSeparator: " ", + inputFocus: true, + clickFire: false, + highlight: function(value, term) { + return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); + }, + scroll: true, + scrollHeight: 180, + scrollJumpPosition: true +}; + +$.Autocompleter.Cache = function(options) { + + var data = {}; + var length = 0; + + function matchSubset(s, sub) { + return (new RegExp(sub.toUpperCase().replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1").replace(/[A-Z0-9]/g, function(m, offset) { + return offset === 0 ? '(?:' + m + '|^' + m.toLowerCase() + ')' : '(?:.*' + m + '|' + m.toLowerCase() + ')'; + }))).test(s); // find by initials + }; + + function add(q, value) { + if (length > options.cacheLength){ + flush(); + } + if (!data[q]){ + length++; + } + data[q] = value; + } + + function populate(){ + if( !options.data ) return false; + // track the matches + var stMatchSets = {}, + nullData = 0; + + // no url was specified, we need to adjust the cache length to make sure it fits the local data store + if( !options.url ) options.cacheLength = 1; + + // track all options for minChars = 0 + stMatchSets[""] = []; + + // loop through the array and create a lookup structure + for ( var i = 0, ol = options.data.length; i < ol; i++ ) { + var rawValue = options.data[i]; + // if rawValue is a string, make an array otherwise just reference the array + rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; + + var value = options.formatMatch(rawValue, i+1, options.data.length); + if ( typeof(value) === 'undefined' || value === false ) + continue; + + var firstChar = value.charAt(0).toLowerCase(); + // if no lookup array for this character exists, look it up now + if( !stMatchSets[firstChar] ) + stMatchSets[firstChar] = []; + + // if the match is a string + var row = { + value: value, + data: rawValue, + result: options.formatResult && options.formatResult(rawValue) || value + }; + + // push the current match into the set list + stMatchSets[firstChar].push(row); + + // keep track of minChars zero items + if ( nullData++ < options.max ) { + stMatchSets[""].push(row); + } + }; + + // add the data items to the cache + $.each(stMatchSets, function(i, value) { + // increase the cache size + options.cacheLength++; + // add to the cache + add(i, value); + }); + } + + // populate any existing data + setTimeout(populate, 25); + + function flush(){ + data = {}; + length = 0; + } + + return { + flush: flush, + add: add, + populate: populate, + load: function(q) { + if (!options.cacheLength || !length) + return null; + /* + * if dealing w/local data and matchContains than we must make sure + * to loop through all the data collections looking for matches + */ + if( !options.url && options.matchContains ){ + // track all matches + var csub = []; + // loop through all the data grids for matches + for( var k in data ){ + // don't search through the stMatchSets[""] (minChars: 0) cache + // this prevents duplicates + if( k.length > 0 ){ + var c = data[k]; + $.each(c, function(i, x) { + // if we've got a match, add it to the array + if (matchSubset(x.value, q)) { + csub.push(x); + } + }); + } + } + return csub; + } else + // if the exact item exists, use it + if (data[q]){ + return data[q]; + } else + if (options.matchSubset) { + for (var i = q.length - 1; i >= options.minChars; i--) { + var c = data[q.substr(0, i)]; + if (c) { + var csub = []; + $.each(c, function(i, x) { + if (matchSubset(x.value, q)) { + csub[csub.length] = x; + } + }); + return csub; + } + } + } + return null; + } + }; +}; + +$.Autocompleter.Select = function (options, input, select, config) { + var CLASSES = { + ACTIVE: "ac_over" + }; + + var listItems, + active = -1, + data, + term = "", + needsInit = true, + element, + list; + + // Create results + function init() { + if (!needsInit) + return; + element = $("
") + .hide() + .addClass(options.resultsClass) + .css("position", "absolute") + .appendTo(document.body) + .hover(function(event) { + // Browsers except FF do not fire mouseup event on scrollbars, resulting in mouseDownOnSelect remaining true, and results list not always hiding. + if($(this).is(":visible")) { + input.focus(); + } + config.mouseDownOnSelect = false; + }); + + list = $("
    ").appendTo(element).mouseover( function(event) { + if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { + active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); + $(target(event)).addClass(CLASSES.ACTIVE); + } + }).click(function(event) { + $(target(event)).addClass(CLASSES.ACTIVE); + select(); + if( options.inputFocus ) + input.focus(); + return false; + }).mousedown(function() { + config.mouseDownOnSelect = true; + }).mouseup(function() { + config.mouseDownOnSelect = false; + }); + + if( options.width > 0 ) + element.css("width", options.width); + + needsInit = false; + } + + function target(event) { + var element = event.target; + while(element && element.tagName != "LI") + element = element.parentNode; + // more fun with IE, sometimes event.target is empty, just ignore it then + if(!element) + return []; + return element; + } + + function moveSelect(step) { + listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); + movePosition(step); + var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); + if(options.scroll) { + var offset = 0; + listItems.slice(0, active).each(function() { + offset += this.offsetHeight; + }); + if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { + list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight()); + } else if(offset < list.scrollTop()) { + list.scrollTop(offset); + } + } + }; + + function movePosition(step) { + if (options.scrollJumpPosition || (!options.scrollJumpPosition && !((step < 0 && active == 0) || (step > 0 && active == listItems.length - 1)) )) { + active += step; + if (active < 0) { + active = listItems.length - 1; + } else if (active >= listItems.length) { + active = 0; + } + } + } + + + function limitNumberOfItems(available) { + return options.max && options.max < available + ? options.max + : available; + } + + function fillList() { + list.empty(); + var max = limitNumberOfItems(data.length); + for (var i=0; i < max; i++) { + if (!data[i]) + continue; + var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term); + if ( formatted === false ) + continue; + var li = $("
  • ").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0]; + $.data(li, "ac_data", data[i]); + } + listItems = list.find("li"); + if ( options.selectFirst ) { + listItems.slice(0, 1).addClass(CLASSES.ACTIVE); + active = 0; + } + // apply bgiframe if available + if ( $.fn.bgiframe ) + list.bgiframe(); + } + + return { + display: function(d, q) { + init(); + data = d; + term = q; + fillList(); + }, + next: function() { + moveSelect(1); + }, + prev: function() { + moveSelect(-1); + }, + pageUp: function() { + if (active != 0 && active - 8 < 0) { + moveSelect( -active ); + } else { + moveSelect(-8); + } + }, + pageDown: function() { + if (active != listItems.length - 1 && active + 8 > listItems.length) { + moveSelect( listItems.length - 1 - active ); + } else { + moveSelect(8); + } + }, + hide: function() { + element && element.hide(); + listItems && listItems.removeClass(CLASSES.ACTIVE); + active = -1; + }, + visible : function() { + return element && element.is(":visible"); + }, + current: function() { + return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); + }, + show: function() { + var offset = $(input).offset(); + element.css({ + width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(), + top: offset.top + input.offsetHeight, + left: offset.left + }).show(); + if(options.scroll) { + list.scrollTop(0); + list.css({ + maxHeight: options.scrollHeight, + overflow: 'auto' + }); + + if(navigator.userAgent.indexOf("MSIE") != -1 && typeof document.body.style.maxHeight === "undefined") { + var listHeight = 0; + listItems.each(function() { + listHeight += this.offsetHeight; + }); + var scrollbarsVisible = listHeight > options.scrollHeight; + list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight ); + if (!scrollbarsVisible) { + // IE doesn't recalculate width when scrollbar disappears + listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) ); + } + } + + } + }, + selected: function() { + var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); + return selected && selected.length && $.data(selected[0], "ac_data"); + }, + emptyList: function (){ + list && list.empty(); + }, + unbind: function() { + element && element.remove(); + } + }; +}; + +$.fn.selection = function(start, end) { + if (start !== undefined) { + return this.each(function() { + if( this.createTextRange ){ + var selRange = this.createTextRange(); + if (end === undefined || start == end) { + selRange.move("character", start); + selRange.select(); + } else { + selRange.collapse(true); + selRange.moveStart("character", start); + selRange.moveEnd("character", end); + selRange.select(); + } + } else if( this.setSelectionRange ){ + this.setSelectionRange(start, end); + } else if( this.selectionStart ){ + this.selectionStart = start; + this.selectionEnd = end; + } + }); + } + var field = this[0]; + if ( field.createTextRange ) { + var range = document.selection.createRange(), + orig = field.value, + teststring = "<->", + textLength = range.text.length; + range.text = teststring; + var caretAt = field.value.indexOf(teststring); + field.value = orig; + this.selection(caretAt, caretAt + textLength); + return { + start: caretAt, + end: caretAt + textLength + } + } else if( field.selectionStart !== undefined ){ + return { + start: field.selectionStart, + end: field.selectionEnd + } + } +}; + +})(jQuery); diff --git a/apigen/theme-woocommerce/js/jquery.cookie.js b/apigen/theme-woocommerce/js/jquery.cookie.js new file mode 100644 index 00000000000..3838d7ed20c --- /dev/null +++ b/apigen/theme-woocommerce/js/jquery.cookie.js @@ -0,0 +1,114 @@ +/*! + * jQuery Cookie Plugin v1.4.1 + * https://github.com/carhartl/jquery-cookie + * + * Copyright 2006, 2014 Klaus Hartl + * Released under the MIT license + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD (Register as an anonymous module) + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + + var pluses = /\+/g; + + function encode(s) { + return config.raw ? s : encodeURIComponent(s); + } + + function decode(s) { + return config.raw ? s : decodeURIComponent(s); + } + + function stringifyCookieValue(value) { + return encode(config.json ? JSON.stringify(value) : String(value)); + } + + function parseCookieValue(s) { + if (s.indexOf('"') === 0) { + // This is a quoted cookie as according to RFC2068, unescape... + s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } + + try { + // Replace server-side written pluses with spaces. + // If we can't decode the cookie, ignore it, it's unusable. + // If we can't parse the cookie, ignore it, it's unusable. + s = decodeURIComponent(s.replace(pluses, ' ')); + return config.json ? JSON.parse(s) : s; + } catch(e) {} + } + + function read(s, converter) { + var value = config.raw ? s : parseCookieValue(s); + return $.isFunction(converter) ? converter(value) : value; + } + + var config = $.cookie = function (key, value, options) { + + // Write + + if (arguments.length > 1 && !$.isFunction(value)) { + options = $.extend({}, config.defaults, options); + + if (typeof options.expires === 'number') { + var days = options.expires, t = options.expires = new Date(); + t.setMilliseconds(t.getMilliseconds() + days * 864e+5); + } + + return (document.cookie = [ + encode(key), '=', stringifyCookieValue(value), + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.secure ? '; secure' : '' + ].join('')); + } + + // Read + + var result = key ? undefined : {}, + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling $.cookie(). + cookies = document.cookie ? document.cookie.split('; ') : [], + i = 0, + l = cookies.length; + + for (; i < l; i++) { + var parts = cookies[i].split('='), + name = decode(parts.shift()), + cookie = parts.join('='); + + if (key === name) { + // If second argument (value) is a function it's a converter... + result = read(cookie, value); + break; + } + + // Prevent storing a cookie that we couldn't decode. + if (!key && (cookie = read(cookie)) !== undefined) { + result[name] = cookie; + } + } + + return result; + }; + + config.defaults = {}; + + $.removeCookie = function (key, options) { + // Must not alter options, thus extending a fresh object... + $.cookie(key, '', $.extend({}, options, { expires: -1 })); + return !$.cookie(key); + }; + +})); diff --git a/apigen/theme-woocommerce/js/jquery.min.js b/apigen/theme-woocommerce/js/jquery.min.js new file mode 100644 index 00000000000..ce1b6b6e0b0 --- /dev/null +++ b/apigen/theme-woocommerce/js/jquery.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license +*/ +(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="
    ",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
    a",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="
    t
    ",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
    ",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t +}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:x.support.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle); +u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("'):E=e('');a.theme?S=e(''):S=e('');if(a.theme&&m){T='"}else if(a.theme){T='"}else m?T='':T='';x=e(T);if(g)if(a.theme){x.css(h);x.addClass("ui-widget-content")}else x.css(l);a.theme||S.css(a.overlayCSS);S.css("position",m?"fixed":"absolute");(n||a.forceIframe)&&E.css("opacity",0);var N=[E,S,x],C=m?e("body"):e(i);e.each(N,function(){this.appendTo(C)});a.theme&&a.draggable&&e.fn.draggable&&x.draggable({handle:".ui-dialog-titlebar",cancel:"li"});var k=s&&(!e.support.boxModel||e("object,embed",m?null:i).length>0);if(r||k){m&&a.allowBodyStretch&&e.support.boxModel&&e("html,body").css("height","100%");if((r||!e.support.boxModel)&&!m)var L=v(i,"borderTopWidth"),A=v(i,"borderLeftWidth"),O=L?"(0 - "+L+")":0,M=A?"(0 - "+A+")":0;e.each(N,function(e,t){var n=t[0].style;n.position="absolute";if(e<2){m?n.setExpression("height","Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:"+a.quirksmodeOffsetHack+') + "px"'):n.setExpression("height",'this.parentNode.offsetHeight + "px"');m?n.setExpression("width",'jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'):n.setExpression("width",'this.parentNode.offsetWidth + "px"');M&&n.setExpression("left",M);O&&n.setExpression("top",O)}else if(a.centerY){m&&n.setExpression("top",'(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');n.marginTop=0}else if(!a.centerY&&m){var r=a.css&&a.css.top?parseInt(a.css.top,10):0,i="((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "+r+') + "px"';n.setExpression("top",i)}})}if(g){a.theme?x.find(".ui-widget-content").append(g):x.append(g);(g.jquery||g.nodeType)&&e(g).show()}(n||a.forceIframe)&&a.showOverlay&&E.show();if(a.fadeIn){var _=a.onBlock?a.onBlock:t,D=a.showOverlay&&!g?_:t,P=g?_:t;a.showOverlay&&S._fadeIn(a.fadeIn,D);g&&x._fadeIn(a.fadeIn,P)}else{a.showOverlay&&S.show();g&&x.show();a.onBlock&&a.onBlock()}c(1,i,a);if(m){o=x[0];u=e(a.focusableElements,o);a.focusInput&&setTimeout(p,20)}else d(x[0],a.centerX,a.centerY);if(a.timeout){var H=setTimeout(function(){m?e.unblockUI(a):e(i).unblock(a)},a.timeout);e(i).data("blockUI.timeout",H)}}function f(t,n){var r,i=t==window,s=e(t),a=s.data("blockUI.history"),f=s.data("blockUI.timeout");if(f){clearTimeout(f);s.removeData("blockUI.timeout")}n=e.extend({},e.blockUI.defaults,n||{});c(0,t,n);if(n.onUnblock===null){n.onUnblock=s.data("blockUI.onUnblock");s.removeData("blockUI.onUnblock")}var h;i?h=e("body").children().filter(".blockUI").add("body > .blockUI"):h=s.find(">.blockUI");if(n.cursorReset){h.length>1&&(h[1].style.cursor=n.cursorReset);h.length>2&&(h[2].style.cursor=n.cursorReset)}i&&(o=u=null);if(n.fadeOut){r=h.length;h.stop().fadeOut(n.fadeOut,function(){--r===0&&l(h,a,n,t)})}else l(h,a,n,t)}function l(t,n,r,i){var s=e(i);if(s.data("blockUI.isBlocked"))return;t.each(function(e,t){this.parentNode&&this.parentNode.removeChild(this)});if(n&&n.el){n.el.style.display=n.display;n.el.style.position=n.position;n.parent&&n.parent.appendChild(n.el);s.removeData("blockUI.history")}s.data("blockUI.static")&&s.css("position","static");typeof r.onUnblock=="function"&&r.onUnblock(i,r);var o=e(document.body),u=o.width(),a=o[0].style.width;o.width(u-1).width(u);o[0].style.width=a}function c(t,n,r){var i=n==window,s=e(n);if(!t&&(i&&!o||!i&&!s.data("blockUI.isBlocked")))return;s.data("blockUI.isBlocked",t);if(!i||!r.bindEvents||t&&!r.showOverlay)return;var u="mousedown mouseup keydown keypress keyup touchstart touchend touchmove";t?e(document).bind(u,r,h):e(document).unbind(u,h)}function h(t){if(t.type==="keydown"&&t.keyCode&&t.keyCode==9&&o&&t.data.constrainTabKey){var n=u,r=!t.shiftKey&&t.target===n[n.length-1],i=t.shiftKey&&t.target===n[0];if(r||i){setTimeout(function(){p(i)},10);return!1}}var s=t.data,a=e(t.target);a.hasClass("blockOverlay")&&s.onOverlayClick&&s.onOverlayClick(t);return a.parents("div."+s.blockMsgClass).length>0?!0:a.parents().children().filter("div.blockUI").length===0}function p(e){if(!u)return;var t=u[e===!0?u.length-1:0];t&&t.focus()}function d(e,t,n){var r=e.parentNode,i=e.style,s=(r.offsetWidth-e.offsetWidth)/2-v(r,"borderLeftWidth"),o=(r.offsetHeight-e.offsetHeight)/2-v(r,"borderTopWidth");t&&(i.left=s>0?s+"px":"0");n&&(i.top=o>0?o+"px":"0")}function v(t,n){return parseInt(e.css(t,n),10)||0}e.fn._fadeIn=e.fn.fadeIn;var t=e.noop||function(){},n=/MSIE/.test(navigator.userAgent),r=/MSIE 6.0/.test(navigator.userAgent)&&!/MSIE 8.0/.test(navigator.userAgent),i=document.documentMode||0,s=e.isFunction(document.createElement("div").style.setExpression);e.blockUI=function(e){a(window,e)};e.unblockUI=function(e){f(window,e)};e.growlUI=function(t,n,r,i){var s=e('
    ');t&&s.append("

    "+t+"

    ");n&&s.append("

    "+n+"

    ");r===undefined&&(r=3e3);var o=function(t){t=t||{};e.blockUI({message:s,fadeIn:typeof t.fadeIn!="undefined"?t.fadeIn:700,fadeOut:typeof t.fadeOut!="undefined"?t.fadeOut:1e3,timeout:typeof t.timeout!="undefined"?t.timeout:r,centerY:!1,showOverlay:!1,onUnblock:i,css:e.blockUI.defaults.growlCSS})};o();var u=s.css("opacity");s.mouseover(function(){o({fadeIn:0,timeout:3e4});var t=e(".blockMsg");t.stop();t.fadeTo(300,1)}).mouseout(function(){e(".blockMsg").fadeOut(1e3)})};e.fn.block=function(t){if(this[0]===window){e.blockUI(t);return this}var n=e.extend({},e.blockUI.defaults,t||{});this.each(function(){var t=e(this);if(n.ignoreIfBlocked&&t.data("blockUI.isBlocked"))return;t.unblock({fadeOut:0})});return this.each(function(){if(e.css(this,"position")=="static"){this.style.position="relative";e(this).data("blockUI.static",!0)}this.style.zoom=1;a(this,t)})};e.fn.unblock=function(t){if(this[0]===window){e.unblockUI(t);return this}return this.each(function(){f(this,t)})};e.blockUI.version=2.66;e.blockUI.defaults={message:"

    Please wait...

    ",title:null,draggable:!0,theme:!1,css:{padding:0,margin:0,width:"30%",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"3px solid #aaa",backgroundColor:"#fff",cursor:"wait"},themedCSS:{width:"30%",top:"40%",left:"35%"},overlayCSS:{backgroundColor:"#000",opacity:.6,cursor:"wait"},cursorReset:"default",growlCSS:{width:"350px",top:"10px",left:"",right:"10px",border:"none",padding:"5px",opacity:.6,cursor:"default",color:"#fff",backgroundColor:"#000","-webkit-border-radius":"10px","-moz-border-radius":"10px","border-radius":"10px"},iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank",forceIframe:!1,baseZ:1e3,centerX:!0,centerY:!0,allowBodyStretch:!0,bindEvents:!0,constrainTabKey:!0,fadeIn:200,fadeOut:400,timeout:0,showOverlay:!0,focusInput:!0,focusableElements:":input:enabled:visible",onBlock:null,onUnblock:null,onOverlayClick:null,quirksmodeOffsetHack:4,blockMsgClass:"blockMsg",ignoreIfBlocked:!1};var o=null,u=[]}typeof define=="function"&&define.amd&&define.amd.jQuery?define(["jquery"],e):e(jQuery)})(); \ No newline at end of file + */ +!function(){"use strict";function e(e){function t(t,n){var s,h,k=t==window,y=n&&n.message!==undefined?n.message:undefined;if(!(n=e.extend({},e.blockUI.defaults,n||{})).ignoreIfBlocked||!e(t).data("blockUI.isBlocked")){if(n.overlayCSS=e.extend({},e.blockUI.defaults.overlayCSS,n.overlayCSS||{}),s=e.extend({},e.blockUI.defaults.css,n.css||{}),n.onOverlayClick&&(n.overlayCSS.cursor="pointer"),h=e.extend({},e.blockUI.defaults.themedCSS,n.themedCSS||{}),y=y===undefined?n.message:y,k&&p&&o(window,{fadeOut:0}),y&&"string"!=typeof y&&(y.parentNode||y.jquery)){var m=y.jquery?y[0]:y,g={};e(t).data("blockUI.history",g),g.el=m,g.parent=m.parentNode,g.display=m.style.display,g.position=m.style.position,g.parent&&g.parent.removeChild(m)}e(t).data("blockUI.onUnblock",n.onUnblock);var v,I,w,U,x=n.baseZ;v=e(r||n.forceIframe?'':''),I=e(n.theme?'':''),n.theme&&k?(U='"):n.theme?(U='"):U=k?'':'',w=e(U),y&&(n.theme?(w.css(h),w.addClass("ui-widget-content")):w.css(s)),n.theme||I.css(n.overlayCSS),I.css("position",k?"fixed":"absolute"),(r||n.forceIframe)&&v.css("opacity",0);var C=[v,I,w],S=e(k?"body":t);e.each(C,function(){this.appendTo(S)}),n.theme&&n.draggable&&e.fn.draggable&&w.draggable({handle:".ui-dialog-titlebar",cancel:"li"});var O=f&&(!e.support.boxModel||e("object,embed",k?null:t).length>0);if(u||O){if(k&&n.allowBodyStretch&&e.support.boxModel&&e("html,body").css("height","100%"),(u||!e.support.boxModel)&&!k)var E=a(t,"borderTopWidth"),T=a(t,"borderLeftWidth"),M=E?"(0 - "+E+")":0,B=T?"(0 - "+T+")":0;e.each(C,function(e,t){var o=t[0].style;if(o.position="absolute",e<2)k?o.setExpression("height","Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:"+n.quirksmodeOffsetHack+') + "px"'):o.setExpression("height",'this.parentNode.offsetHeight + "px"'),k?o.setExpression("width",'jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"'):o.setExpression("width",'this.parentNode.offsetWidth + "px"'),B&&o.setExpression("left",B),M&&o.setExpression("top",M);else if(n.centerY)k&&o.setExpression("top",'(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'),o.marginTop=0;else if(!n.centerY&&k){var i="((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "+(n.css&&n.css.top?parseInt(n.css.top,10):0)+') + "px"';o.setExpression("top",i)}})}if(y&&(n.theme?w.find(".ui-widget-content").append(y):w.append(y),(y.jquery||y.nodeType)&&e(y).show()),(r||n.forceIframe)&&n.showOverlay&&v.show(),n.fadeIn){var j=n.onBlock?n.onBlock:c,H=n.showOverlay&&!y?j:c,z=y?j:c;n.showOverlay&&I._fadeIn(n.fadeIn,H),y&&w._fadeIn(n.fadeIn,z)}else n.showOverlay&&I.show(),y&&w.show(),n.onBlock&&n.onBlock.bind(w)();if(i(1,t,n),k?(p=w[0],b=e(n.focusableElements,p),n.focusInput&&setTimeout(l,20)):d(w[0],n.centerX,n.centerY),n.timeout){var W=setTimeout(function(){k?e.unblockUI(n):e(t).unblock(n)},n.timeout);e(t).data("blockUI.timeout",W)}}}function o(t,o){var s,l=t==window,d=e(t),a=d.data("blockUI.history"),c=d.data("blockUI.timeout");c&&(clearTimeout(c),d.removeData("blockUI.timeout")),o=e.extend({},e.blockUI.defaults,o||{}),i(0,t,o),null===o.onUnblock&&(o.onUnblock=d.data("blockUI.onUnblock"),d.removeData("blockUI.onUnblock"));var r;r=l?e(document.body).children().filter(".blockUI").add("body > .blockUI"):d.find(">.blockUI"),o.cursorReset&&(r.length>1&&(r[1].style.cursor=o.cursorReset),r.length>2&&(r[2].style.cursor=o.cursorReset)),l&&(p=b=null),o.fadeOut?(s=r.length,r.stop().fadeOut(o.fadeOut,function(){0==--s&&n(r,a,o,t)})):n(r,a,o,t)}function n(t,o,n,i){var s=e(i);if(!s.data("blockUI.isBlocked")){t.each(function(e,t){this.parentNode&&this.parentNode.removeChild(this)}),o&&o.el&&(o.el.style.display=o.display,o.el.style.position=o.position,o.el.style.cursor="default",o.parent&&o.parent.appendChild(o.el),s.removeData("blockUI.history")),s.data("blockUI.static")&&s.css("position","static"),"function"==typeof n.onUnblock&&n.onUnblock(i,n);var l=e(document.body),d=l.width(),a=l[0].style.width;l.width(d-1).width(d),l[0].style.width=a}}function i(t,o,n){var i=o==window,l=e(o);if((t||(!i||p)&&(i||l.data("blockUI.isBlocked")))&&(l.data("blockUI.isBlocked",t),i&&n.bindEvents&&(!t||n.showOverlay))){var d="mousedown mouseup keydown keypress keyup touchstart touchend touchmove";t?e(document).bind(d,n,s):e(document).unbind(d,s)}}function s(t){if("keydown"===t.type&&t.keyCode&&9==t.keyCode&&p&&t.data.constrainTabKey){var o=b,n=!t.shiftKey&&t.target===o[o.length-1],i=t.shiftKey&&t.target===o[0];if(n||i)return setTimeout(function(){l(i)},10),!1}var s=t.data,d=e(t.target);return d.hasClass("blockOverlay")&&s.onOverlayClick&&s.onOverlayClick(t),d.parents("div."+s.blockMsgClass).length>0||0===d.parents().children().filter("div.blockUI").length}function l(e){if(b){var t=b[!0===e?b.length-1:0];t&&t.focus()}}function d(e,t,o){var n=e.parentNode,i=e.style,s=(n.offsetWidth-e.offsetWidth)/2-a(n,"borderLeftWidth"),l=(n.offsetHeight-e.offsetHeight)/2-a(n,"borderTopWidth");t&&(i.left=s>0?s+"px":"0"),o&&(i.top=l>0?l+"px":"0")}function a(t,o){return parseInt(e.css(t,o),10)||0}e.fn._fadeIn=e.fn.fadeIn;var c=e.noop||function(){},r=/MSIE/.test(navigator.userAgent),u=/MSIE 6.0/.test(navigator.userAgent)&&!/MSIE 8.0/.test(navigator.userAgent),f=(document.documentMode,e.isFunction(document.createElement("div").style.setExpression));e.blockUI=function(e){t(window,e)},e.unblockUI=function(e){o(window,e)},e.growlUI=function(t,o,n,i){var s=e('
    ');t&&s.append("

    "+t+"

    "),o&&s.append("

    "+o+"

    "),n===undefined&&(n=3e3);var l=function(t){t=t||{},e.blockUI({message:s,fadeIn:"undefined"!=typeof t.fadeIn?t.fadeIn:700,fadeOut:"undefined"!=typeof t.fadeOut?t.fadeOut:1e3,timeout:"undefined"!=typeof t.timeout?t.timeout:n,centerY:!1,showOverlay:!1,onUnblock:i,css:e.blockUI.defaults.growlCSS})};l();s.css("opacity");s.mouseover(function(){l({fadeIn:0,timeout:3e4});var t=e(".blockMsg");t.stop(),t.fadeTo(300,1)}).mouseout(function(){e(".blockMsg").fadeOut(1e3)})},e.fn.block=function(o){if(this[0]===window)return e.blockUI(o),this;var n=e.extend({},e.blockUI.defaults,o||{});return this.each(function(){var t=e(this);n.ignoreIfBlocked&&t.data("blockUI.isBlocked")||t.unblock({fadeOut:0})}),this.each(function(){"static"==e.css(this,"position")&&(this.style.position="relative",e(this).data("blockUI.static",!0)),this.style.zoom=1,t(this,o)})},e.fn.unblock=function(t){return this[0]===window?(e.unblockUI(t),this):this.each(function(){o(this,t)})},e.blockUI.version=2.7,e.blockUI.defaults={message:"

    Please wait...

    ",title:null,draggable:!0,theme:!1,css:{padding:0,margin:0,width:"30%",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"3px solid #aaa",backgroundColor:"#fff",cursor:"wait"},themedCSS:{width:"30%",top:"40%",left:"35%"},overlayCSS:{backgroundColor:"#000",opacity:.6,cursor:"wait"},cursorReset:"default",growlCSS:{width:"350px",top:"10px",left:"",right:"10px",border:"none",padding:"5px",opacity:.6,cursor:"default",color:"#fff",backgroundColor:"#000","-webkit-border-radius":"10px","-moz-border-radius":"10px","border-radius":"10px"},iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank",forceIframe:!1,baseZ:1e3,centerX:!0,centerY:!0,allowBodyStretch:!0,bindEvents:!0,constrainTabKey:!0,fadeIn:200,fadeOut:400,timeout:0,showOverlay:!0,focusInput:!0,focusableElements:":input:enabled:visible",onBlock:null,onUnblock:null,onOverlayClick:null,quirksmodeOffsetHack:4,blockMsgClass:"blockMsg",ignoreIfBlocked:!1};var p=null,b=[]}"function"==typeof define&&define.amd&&define.amd.jQuery?define(["jquery"],e):e(jQuery)}(); \ No newline at end of file diff --git a/assets/js/jquery-cookie/jquery.cookie.js b/assets/js/jquery-cookie/jquery.cookie.js index 92719000876..5ff7fef9d6b 100644 --- a/assets/js/jquery-cookie/jquery.cookie.js +++ b/assets/js/jquery-cookie/jquery.cookie.js @@ -1,5 +1,5 @@ /*! - * jQuery Cookie Plugin v1.4.0 + * jQuery Cookie Plugin v1.4.1 * https://github.com/carhartl/jquery-cookie * * Copyright 2013 Klaus Hartl @@ -7,10 +7,13 @@ */ (function (factory) { if (typeof define === 'function' && define.amd) { - // AMD. Register as anonymous module. + // AMD define(['jquery'], factory); + } else if (typeof exports === 'object') { + // CommonJS + factory(require('jquery')); } else { - // Browser globals. + // Browser globals factory(jQuery); } }(function ($) { @@ -38,13 +41,8 @@ try { // Replace server-side written pluses with spaces. // If we can't decode the cookie, ignore it, it's unusable. - s = decodeURIComponent(s.replace(pluses, ' ')); - } catch(e) { - return; - } - - try { // If we can't parse the cookie, ignore it, it's unusable. + s = decodeURIComponent(s.replace(pluses, ' ')); return config.json ? JSON.parse(s) : s; } catch(e) {} } @@ -57,12 +55,13 @@ var config = $.cookie = function (key, value, options) { // Write + if (value !== undefined && !$.isFunction(value)) { options = $.extend({}, config.defaults, options); if (typeof options.expires === 'number') { var days = options.expires, t = options.expires = new Date(); - t.setDate(t.getDate() + days); + t.setTime(+t + days * 864e+5); } return (document.cookie = [ @@ -106,12 +105,13 @@ config.defaults = {}; $.removeCookie = function (key, options) { - if ($.cookie(key) !== undefined) { - // Must not alter options, thus extending a fresh object... - $.cookie(key, '', $.extend({}, options, { expires: -1 })); - return true; + if ($.cookie(key) === undefined) { + return false; } - return false; + + // Must not alter options, thus extending a fresh object... + $.cookie(key, '', $.extend({}, options, { expires: -1 })); + return !$.cookie(key); }; -})); +})); \ No newline at end of file diff --git a/assets/js/jquery-cookie/jquery.cookie.min.js b/assets/js/jquery-cookie/jquery.cookie.min.js index e68425fb7cd..3a05387971d 100644 --- a/assets/js/jquery-cookie/jquery.cookie.min.js +++ b/assets/js/jquery-cookie/jquery.cookie.min.js @@ -1,7 +1,8 @@ /*! - * jQuery Cookie Plugin v1.4.0 + * jQuery Cookie Plugin v1.4.1 * https://github.com/carhartl/jquery-cookie * * Copyright 2013 Klaus Hartl * Released under the MIT license - */(function(e){typeof define=="function"&&define.amd?define(["jquery"],e):e(jQuery)})(function(e){function n(e){return u.raw?e:encodeURIComponent(e)}function r(e){return u.raw?e:decodeURIComponent(e)}function i(e){return n(u.json?JSON.stringify(e):String(e))}function s(e){e.indexOf('"')===0&&(e=e.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{e=decodeURIComponent(e.replace(t," "))}catch(n){return}try{return u.json?JSON.parse(e):e}catch(n){}}function o(t,n){var r=u.raw?t:s(t);return e.isFunction(n)?n(r):r}var t=/\+/g,u=e.cookie=function(t,s,a){if(s!==undefined&&!e.isFunction(s)){a=e.extend({},u.defaults,a);if(typeof a.expires=="number"){var f=a.expires,l=a.expires=new Date;l.setDate(l.getDate()+f)}return document.cookie=[n(t),"=",i(s),a.expires?"; expires="+a.expires.toUTCString():"",a.path?"; path="+a.path:"",a.domain?"; domain="+a.domain:"",a.secure?"; secure":""].join("")}var c=t?undefined:{},h=document.cookie?document.cookie.split("; "):[];for(var p=0,d=h.length;p=1?"rgb("+[r.r,r.g,r.b].join(",")+")":"rgba("+[r.r,r.g,r.b,r.a].join(",")+")"},r.normalize=function(){function t(t,i,e){return ie?e:i}return r.r=t(0,parseInt(r.r),255),r.g=t(0,parseInt(r.g),255),r.b=t(0,parseInt(r.b),255),r.a=t(0,r.a,1),r},r.clone=function(){return t.color.make(r.r,r.b,r.g,r.a)},r.normalize()},t.color.extract=function(i,e){var o;do{if(""!=(o=i.css(e).toLowerCase())&&"transparent"!=o)break;i=i.parent()}while(!t.nodeName(i.get(0),"body"));return"rgba(0, 0, 0, 0)"==o&&(o="transparent"),t.color.parse(o)},t.color.parse=function(e){var o,n=t.color.make;if(o=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(e))return n(parseInt(o[1],10),parseInt(o[2],10),parseInt(o[3],10));if(o=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(e))return n(parseInt(o[1],10),parseInt(o[2],10),parseInt(o[3],10),parseFloat(o[4]));if(o=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(e))return n(2.55*parseFloat(o[1]),2.55*parseFloat(o[2]),2.55*parseFloat(o[3]));if(o=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(e))return n(2.55*parseFloat(o[1]),2.55*parseFloat(o[2]),2.55*parseFloat(o[3]),parseFloat(o[4]));if(o=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(e))return n(parseInt(o[1],16),parseInt(o[2],16),parseInt(o[3],16));if(o=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(e))return n(parseInt(o[1]+o[1],16),parseInt(o[2]+o[2],16),parseInt(o[3]+o[3],16));var r=t.trim(e).toLowerCase();return"transparent"==r?n(255,255,255,0):(o=i[r]||[0,0,0],n(o[0],o[1],o[2]))};var i={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}}(jQuery),function(t){function i(i,e){var o=e.children("."+i)[0];if(null==o&&(o=document.createElement("canvas"),o.className=i,t(o).css({direction:"ltr",position:"absolute",left:0,top:0}).appendTo(e),!o.getContext)){if(!window.G_vmlCanvasManager)throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.");o=window.G_vmlCanvasManager.initElement(o)}this.element=o;var n=this.context=o.getContext("2d"),r=window.devicePixelRatio||1,a=n.webkitBackingStorePixelRatio||n.mozBackingStorePixelRatio||n.msBackingStorePixelRatio||n.oBackingStorePixelRatio||n.backingStorePixelRatio||1;this.pixelRatio=r/a,this.resize(e.width(),e.height()),this.textContainer=null,this.text={},this._textCache={}}function e(e,n,r,a){function l(t,i){i=[ft].concat(i);for(var e=0;eo&&(o=n))}e<=o&&(e=o+1);var r,a=[],l=K.colors,s=l.length,c=0;for(i=0;i=0?c<.5?-c-.2:0:-c),a[i]=r.scale("rgb",1+c);var h,u=0;for(i=0;i<$.length;++i){if(null==(h=$[i]).color?(h.color=a[u].toString(),++u):"number"==typeof h.color&&(h.color=a[h.color].toString()),null==h.lines.show){var p,m=!0;for(p in h)if(h[p]&&h[p].show){m=!1;break}m&&(h.lines.show=!0)}null==h.lines.zero&&(h.lines.zero=!!h.lines.fill),h.xaxis=d(nt,f(h,"x")),h.yaxis=d(rt,f(h,"y"))}}function m(){function i(t,i,e){it.datamax&&e!=b&&(t.datamax=e)}var e,o,n,r,a,s,c,f,u,d,p,m,x=Number.POSITIVE_INFINITY,g=Number.NEGATIVE_INFINITY,b=Number.MAX_VALUE;for(t.each(h(),function(t,i){i.datamin=x,i.datamax=g,i.used=!1}),e=0;e<$.length;++e)(a=$[e]).datapoints={points:[]},l(ct.processRawData,[a,a.data,a.datapoints]);for(e=0;e<$.length;++e){if(a=$[e],p=a.data,!(m=a.datapoints.format)){if((m=[]).push({x:!0,number:!0,required:!0}),m.push({y:!0,number:!0,required:!0}),a.bars.show||a.lines.show&&a.lines.fill){var v=!!(a.bars.show&&a.bars.zero||a.lines.show&&a.lines.zero);m.push({y:!0,number:!0,required:!1,defaultValue:0,autoscale:v}),a.bars.horizontal&&(delete m[m.length-1].y,m[m.length-1].x=!0)}a.datapoints.format=m}if(null==a.datapoints.pointsize){a.datapoints.pointsize=m.length,c=a.datapoints.pointsize,s=a.datapoints.points;var k=a.lines.show&&a.lines.steps;for(a.xaxis.used=a.yaxis.used=!0,o=n=0;o0&&null!=s[n-c]&&s[n-c]!=s[n]&&s[n-c+1]!=s[n+1]){for(r=0;rM&&(M=f)),u.y&&(fC&&(C=f)));if(a.bars.show){var S;switch(a.bars.align){case"left":S=0;break;case"right":S=-a.bars.barWidth;break;case"center":S=-a.bars.barWidth/2;break;default:throw new Error("Invalid bar alignment: "+a.bars.align)}a.bars.horizontal?(T+=S,C+=S+a.bars.barWidth):(w+=S,M+=S+a.bars.barWidth)}i(a.xaxis,w,M),i(a.yaxis,T,C)}t.each(h(),function(t,i){i.datamin==x&&(i.datamin=null),i.datamax==g&&(i.datamax=null)})}function x(t){function i(t){return t}var e,o,n=t.options.transform||i,r=t.options.inverseTransform;"x"==t.direction?(e=t.scale=lt/Math.abs(n(t.max)-n(t.min)),o=Math.min(n(t.max),n(t.min))):(e=t.scale=st/Math.abs(n(t.max)-n(t.min)),e=-e,o=Math.max(n(t.max),n(t.min))),t.p2c=n==i?function(t){return(t-o)*e}:function(t){return(n(t)-o)*e},t.c2p=r?function(t){return r(o+t/e)}:function(t){return o+t/e}}function g(t){var i=t.options,e=t.ticks||[],o=i.labelWidth||0,n=i.labelHeight||0,r=o||"x"==t.direction?Math.floor(Z.width/(e.length||1)):null;legacyStyles=t.direction+"Axis "+t.direction+t.n+"Axis",layer="flot-"+t.direction+"-axis flot-"+t.direction+t.n+"-axis "+legacyStyles,font=i.font||"flot-tick-label tickLabel";for(var a=0;a=0;--i)b(a[i]);k(),t.each(a,function(t,i){v(i)})}lt=Z.width-at.left-at.right,st=Z.height-at.bottom-at.top,t.each(e,function(t,i){x(i)}),o&&A(),E()}function w(t){var i=t.options,e=+(null!=i.min?i.min:t.datamin),o=+(null!=i.max?i.max:t.datamax),n=o-e;if(0==n){var r=0==o?1:.01;null==i.min&&(e-=r),null!=i.max&&null==i.min||(o+=r)}else{var a=i.autoscaleMargin;null!=a&&(null==i.min&&(e-=n*a)<0&&null!=t.datamin&&t.datamin>=0&&(e=0),null==i.max&&(o+=n*a)>0&&null!=t.datamax&&t.datamax<=0&&(o=0))}t.min=e,t.max=o}function T(i){var e,n=i.options;e="number"==typeof n.ticks&&n.ticks>0?n.ticks:.3*Math.sqrt("x"==i.direction?Z.width:Z.height);var r=(i.max-i.min)/e,a=-Math.floor(Math.log(r)/Math.LN10),l=n.tickDecimals;null!=l&&a>l&&(a=l);var s,c=Math.pow(10,-a),f=r/c;if(f<1.5?s=1:f<3?(s=2,f>2.25&&(null==l||a+1<=l)&&(s=2.5,++a)):s=f<7.5?5:10,s*=c,null!=n.minTickSize&&s0&&(null==n.min&&(i.min=Math.min(i.min,u[0])),null==n.max&&u.length>1&&(i.max=Math.max(i.max,u[u.length-1]))),i.tickGenerator=function(t){var i,e,o=[];for(e=0;e1&&/\..*0$/.test((p[1]-p[0]).toFixed(d))||(i.tickDecimals=d)}}}}function M(i){var e=i.options.ticks,o=[];null==e||"number"==typeof e&&e>0?o=i.tickGenerator(i):e&&(o=t.isFunction(e)?e(i):e);var n,r;for(i.ticks=[],n=0;n1&&(a=l[1])):r=+l,null==a&&(a=i.tickFormatter(r,i)),isNaN(r)||i.ticks.push({v:r,label:a})}}function C(t,i){t.options.autoscaleMargin&&i.length>0&&(null==t.options.min&&(t.min=Math.min(t.min,i[0].v)),null==t.options.max&&i.length>1&&(t.max=Math.max(t.max,i[i.length-1].v)))}function S(){Z.clear(),l(ct.drawBackground,[et]);var t=K.grid;t.show&&t.backgroundColor&&z(),t.show&&!t.aboveData&&I();for(var i=0;i<$.length;++i)l(ct.drawSeries,[et,$[i]]),P($[i]);l(ct.draw,[et]),t.show&&t.aboveData&&I(),Z.render(),_()}function W(t,i){for(var e,o,n,r,a=h(),l=0;ln){var s=o;o=n,n=s}return{from:o,to:n,axis:e}}function z(){et.save(),et.translate(at.left,at.top),et.fillStyle=J(K.grid.backgroundColor,st,0,"rgba(255, 255, 255, 0)"),et.fillRect(0,0,lt,st),et.restore()}function I(){var i,e,o,n;et.save(),et.translate(at.left,at.top);var r=K.grid.markings;if(r)for(t.isFunction(r)&&((e=ft.getAxes()).xmin=e.xaxis.min,e.xmax=e.xaxis.max,e.ymin=e.yaxis.min,e.ymax=e.yaxis.max,r=r(e)),i=0;il.axis.max||s.tos.axis.max||(l.from=Math.max(l.from,l.axis.min),l.to=Math.min(l.to,l.axis.max),s.from=Math.max(s.from,s.axis.min),s.to=Math.min(s.to,s.axis.max),l.from==l.to&&s.from==s.to||(l.from=l.axis.p2c(l.from),l.to=l.axis.p2c(l.to),s.from=s.axis.p2c(s.from),s.to=s.axis.p2c(s.to),l.from==l.to||s.from==s.to?(et.beginPath(),et.strokeStyle=a.color||K.grid.markingsColor,et.lineWidth=a.lineWidth||K.grid.markingsLineWidth,et.moveTo(l.from,s.from),et.lineTo(l.to,s.to),et.stroke()):(et.fillStyle=a.color||K.grid.markingsColor,et.fillRect(l.from,s.to,l.to-l.from,s.from-s.to))))}e=h(),o=K.grid.borderWidth;for(var c=0;cm.max||"full"==g&&("object"==typeof o&&o[m.position]>0||o>0)&&(b==m.min||b==m.max)||("x"==m.direction?(f=m.p2c(b),p="full"==g?-st:g,"top"==m.position&&(p=-p)):(u=m.p2c(b),d="full"==g?-lt:g,"left"==m.position&&(d=-d)),1==et.lineWidth&&("x"==m.direction?f=Math.floor(f)+.5:u=Math.floor(u)+.5),et.moveTo(f,u),et.lineTo(f+d,u+p))}et.stroke()}}o&&(n=K.grid.borderColor,"object"==typeof o||"object"==typeof n?("object"!=typeof o&&(o={top:o,right:o,bottom:o,left:o}),"object"!=typeof n&&(n={top:n,right:n,bottom:n,left:n}),o.top>0&&(et.strokeStyle=n.top,et.lineWidth=o.top,et.beginPath(),et.moveTo(0-o.left,0-o.top/2),et.lineTo(lt,0-o.top/2),et.stroke()),o.right>0&&(et.strokeStyle=n.right,et.lineWidth=o.right,et.beginPath(),et.moveTo(lt+o.right/2,0-o.top),et.lineTo(lt+o.right/2,st),et.stroke()),o.bottom>0&&(et.strokeStyle=n.bottom,et.lineWidth=o.bottom,et.beginPath(),et.moveTo(lt+o.right,st+o.bottom/2),et.lineTo(0,st+o.bottom/2),et.stroke()),o.left>0&&(et.strokeStyle=n.left,et.lineWidth=o.left,et.beginPath(),et.moveTo(0-o.left/2,st+o.bottom),et.lineTo(0-o.left/2,0),et.stroke())):(et.lineWidth=o,et.strokeStyle=K.grid.borderColor,et.strokeRect(-o/2,-o/2,lt+o,st+o))),et.restore()}function A(){t.each(h(),function(t,i){if(i.show&&0!=i.ticks.length){var e,o,n,r,a,l=i.box,s=i.direction+"Axis "+i.direction+i.n+"Axis",c="flot-"+i.direction+"-axis flot-"+i.direction+i.n+"-axis "+s,f=i.options.font||"flot-tick-label tickLabel";Z.removeText(c);for(var h=0;hi.max||("x"==i.direction?(r="center",o=at.left+i.p2c(e.v),"bottom"==i.position?n=l.top+l.padding:(n=l.top+l.height-l.padding,a="bottom")):(a="middle",n=at.top+i.p2c(e.v),"left"==i.position?(o=l.left+l.width-l.padding,r="right"):o=l.left+l.padding),Z.addText(c,o,n,e.label,f,null,null,r,a))}})}function P(t){t.lines.show&&F(t),t.bars.show&&L(t),t.points.show&&N(t)}function F(t){function i(t,i,e,o,n){var r=t.points,a=t.pointsize,l=null,s=null;et.beginPath();for(var c=a;c=d&&h>n.max){if(d>n.max)continue;f=(n.max-h)/(d-h)*(u-f)+f,h=n.max}else if(d>=h&&d>n.max){if(h>n.max)continue;u=(n.max-h)/(d-h)*(u-f)+f,d=n.max}if(f<=u&&f=u&&f>o.max){if(u>o.max)continue;h=(o.max-f)/(u-f)*(d-h)+h,f=o.max}else if(u>=f&&u>o.max){if(f>o.max)continue;d=(o.max-f)/(u-f)*(d-h)+h,u=o.max}f==l&&h==s||et.moveTo(o.p2c(f)+i,n.p2c(h)+e),l=u,s=d,et.lineTo(o.p2c(u)+i,n.p2c(d)+e)}}et.stroke()}et.save(),et.translate(at.left,at.top),et.lineJoin="round";var e=t.lines.lineWidth,o=t.shadowSize;if(e>0&&o>0){et.lineWidth=o,et.strokeStyle="rgba(0,0,0,0.1)";var n=Math.PI/18;i(t.datapoints,Math.sin(n)*(e/2+o/2),Math.cos(n)*(e/2+o/2),t.xaxis,t.yaxis),et.lineWidth=o/2,i(t.datapoints,Math.sin(n)*(e/2+o/4),Math.cos(n)*(e/2+o/4),t.xaxis,t.yaxis)}et.lineWidth=e,et.strokeStyle=t.color;var r=O(t.lines,t.color,0,st);r&&(et.fillStyle=r,function(t,i,e){for(var o=t.points,n=t.pointsize,r=Math.min(Math.max(0,e.min),e.max),a=0,l=!1,s=1,c=0,f=0;!(n>0&&a>o.length+n);){var h=o[(a+=n)-n],u=o[a-n+s],d=o[a],p=o[a+s];if(l){if(n>0&&null!=h&&null==d){f=a,n=-n,s=2;continue}if(n<0&&a==c+n){et.fill(),l=!1,s=1,a=c=f+(n=-n);continue}}if(null!=h&&null!=d){if(h<=d&&h=d&&h>i.max){if(d>i.max)continue;u=(i.max-h)/(d-h)*(p-u)+u,h=i.max}else if(d>=h&&d>i.max){if(h>i.max)continue;p=(i.max-h)/(d-h)*(p-u)+u,d=i.max}if(l||(et.beginPath(),et.moveTo(i.p2c(h),e.p2c(r)),l=!0),u>=e.max&&p>=e.max)et.lineTo(i.p2c(h),e.p2c(e.max)),et.lineTo(i.p2c(d),e.p2c(e.max));else if(u<=e.min&&p<=e.min)et.lineTo(i.p2c(h),e.p2c(e.min)),et.lineTo(i.p2c(d),e.p2c(e.min));else{var m=h,x=d;u<=p&&u=e.min?(h=(e.min-u)/(p-u)*(d-h)+h,u=e.min):p<=u&&p=e.min&&(d=(e.min-u)/(p-u)*(d-h)+h,p=e.min),u>=p&&u>e.max&&p<=e.max?(h=(e.max-u)/(p-u)*(d-h)+h,u=e.max):p>=u&&p>e.max&&u<=e.max&&(d=(e.max-u)/(p-u)*(d-h)+h,p=e.max),h!=m&&et.lineTo(i.p2c(m),e.p2c(u)),et.lineTo(i.p2c(h),e.p2c(u)),et.lineTo(i.p2c(d),e.p2c(p)),d!=x&&(et.lineTo(i.p2c(d),e.p2c(p)),et.lineTo(i.p2c(x),e.p2c(p)))}}}}(t.datapoints,t.xaxis,t.yaxis)),e>0&&i(t.datapoints,0,0,t.xaxis,t.yaxis),et.restore()}function N(t){function i(t,i,e,o,n,r,a,l){for(var s=t.points,c=t.pointsize,f=0;fr.max||ua.max||(et.beginPath(),h=r.p2c(h),u=a.p2c(u)+o,"circle"==l?et.arc(h,u,i,0,n?Math.PI:2*Math.PI,!1):l(et,h,u,i,n),et.closePath(),e&&(et.fillStyle=e,et.fill()),et.stroke())}}et.save(),et.translate(at.left,at.top);var e=t.points.lineWidth,o=t.shadowSize,n=t.points.radius,r=t.points.symbol;if(0==e&&(e=1e-4),e>0&&o>0){var a=o/2;et.lineWidth=a,et.strokeStyle="rgba(0,0,0,0.1)",i(t.datapoints,n,null,a+a/2,!0,t.xaxis,t.yaxis,r),et.strokeStyle="rgba(0,0,0,0.2)",i(t.datapoints,n,null,a/2,!0,t.xaxis,t.yaxis,r)}et.lineWidth=e,et.strokeStyle=t.color,i(t.datapoints,n,O(t.points,t.color),0,!1,t.xaxis,t.yaxis,r),et.restore()}function D(t,i,e,o,n,r,a,l,s,c,f,h){var u,d,p,m,x,g,b,v,k;f?(v=g=b=!0,x=!1,m=i+o,p=i+n,(d=t)<(u=e)&&(k=d,d=u,u=k,x=!0,g=!1)):(x=g=b=!0,v=!1,u=t+o,d=t+n,(m=i)<(p=e)&&(k=m,m=p,p=k,v=!0,b=!1)),dl.max||ms.max||(ul.max&&(d=l.max,g=!1),ps.max&&(m=s.max,b=!1),u=l.p2c(u),p=s.p2c(p),d=l.p2c(d),m=s.p2c(m),a&&(c.beginPath(),c.moveTo(u,p),c.lineTo(u,m),c.lineTo(d,m),c.lineTo(d,p),c.fillStyle=a(p,m),c.fill()),h>0&&(x||g||b||v)&&(c.beginPath(),c.moveTo(u,p+r),x?c.lineTo(u,m+r):c.moveTo(u,m+r),b?c.lineTo(d,m+r):c.moveTo(d,m+r),g?c.lineTo(d,p+r):c.moveTo(d,p+r),v?c.lineTo(u,p+r):c.moveTo(u,p+r),c.stroke()))}function L(t){et.save(),et.translate(at.left,at.top),et.lineWidth=t.bars.lineWidth,et.strokeStyle=t.color;var i;switch(t.bars.align){case"left":i=0;break;case"right":i=-t.bars.barWidth;break;case"center":i=-t.bars.barWidth/2;break;default:throw new Error("Invalid bar alignment: "+t.bars.align)}var e=t.bars.fill?function(i,e){return O(t.bars,t.color,i,e)}:null;!function(i,e,o,n,r,a,l){for(var s=i.points,c=i.pointsize,f=0;f"),n.push(""),a=!0),n.push('")}if(a&&n.push(""),0!=n.length){var h='
    '+f.label+"
    '+n.join("")+"
    ";if(null!=K.legend.container)t(K.legend.container).html(h);else{var u="",d=K.legend.position,p=K.legend.margin;null==p[0]&&(p=[p,p]),"n"==d.charAt(0)?u+="top:"+(p[1]+at.top)+"px;":"s"==d.charAt(0)&&(u+="bottom:"+(p[1]+at.bottom)+"px;"),"e"==d.charAt(1)?u+="right:"+(p[0]+at.right)+"px;":"w"==d.charAt(1)&&(u+="left:"+(p[0]+at.left)+"px;");var m=t('
    '+h.replace('style="','style="position:absolute;'+u+";")+"
    ").appendTo(e);if(0!=K.legend.backgroundOpacity){var x=K.legend.backgroundColor;null==x&&((x=(x=K.grid.backgroundColor)&&"string"==typeof x?t.color.parse(x):t.color.extract(m,"background-color")).a=1,x=x.toString());var g=m.children();t('
    ').prependTo(m).css("opacity",K.legend.backgroundOpacity)}}}}}function R(t,i,e){var o,n,r,a=K.grid.mouseActiveRadius,l=a*a+1,s=null;for(o=$.length-1;o>=0;--o)if(e($[o])){var c=$[o],f=c.xaxis,h=c.yaxis,u=c.datapoints.points,d=f.c2p(t),p=h.c2p(i),m=a/f.scale,x=a/h.scale;if(r=c.datapoints.pointsize,f.options.inverseTransform&&(m=Number.MAX_VALUE),h.options.inverseTransform&&(x=Number.MAX_VALUE),c.lines.show||c.points.show)for(n=0;nm||g-d<-m||b-p>x||b-p<-x)){var v=Math.abs(f.p2c(g)-t),k=Math.abs(h.p2c(b)-i),y=v*v+k*k;y=Math.min(M,g)&&p>=b+w&&p<=b+T:d>=g+w&&d<=g+T&&p>=Math.min(M,b)&&p<=Math.max(M,b))&&(s=[o,n/r]))}}}return s?(o=s[0],n=s[1],r=$[o].datapoints.pointsize,{datapoint:$[o].datapoints.points.slice(n*r,(n+1)*r),dataIndex:n,series:$[o],seriesIndex:o}):null}function j(t){K.grid.hoverable&&G("plothover",t,function(t){return 0!=t.hoverable})}function B(t){K.grid.hoverable&&G("plothover",t,function(t){return!1})}function H(t){G("plotclick",t,function(t){return 0!=t.clickable})}function G(t,i,o){var n=it.offset(),r=i.pageX-n.left-at.left,a=i.pageY-n.top-at.top,l=u({left:r,top:a});l.pageX=i.pageX,l.pageY=i.pageY;var s=R(r,a,o);if(s&&(s.pageX=parseInt(s.series.xaxis.p2c(s.datapoint[0])+n.left+at.left,10),s.pageY=parseInt(s.series.yaxis.p2c(s.datapoint[1])+n.top+at.top,10)),K.grid.autoHighlight){for(var c=0;cr.max||na.max)){var s=i.points.radius+i.points.lineWidth/2;ot.lineWidth=s,ot.strokeStyle=l;var c=1.5*s;o=r.p2c(o),n=a.p2c(n),ot.beginPath(),"circle"==i.points.symbol?ot.arc(o,n,c,0,2*Math.PI,!1):i.points.symbol(ot,o,n,c,!1),ot.closePath(),ot.stroke()}}function U(i,e){var o="string"==typeof i.highlightColor?i.highlightColor:t.color.parse(i.color).scale("a",.5).toString(),n=o,r="left"==i.bars.align?0:-i.bars.barWidth/2;ot.lineWidth=i.bars.lineWidth,ot.strokeStyle=o,D(e[0],e[1],e[2]||0,r,r+i.bars.barWidth,0,function(){return n},i.xaxis,i.yaxis,ot,i.bars.horizontal,i.bars.lineWidth)}function J(i,e,o,n){if("string"==typeof i)return i;for(var r=et.createLinearGradient(0,o,0,e),a=0,l=i.colors.length;a
").css({position:"absolute",top:0,left:0,bottom:0,right:0,"font-size":"smaller",color:"#545454"}).insertAfter(this.element)),e=this.text[i]=t("
").addClass(i).css({position:"absolute",top:0,left:0,bottom:0,right:0}).appendTo(this.textContainer)),e},i.prototype.getTextInfo=function(i,e,o,n,r){var a,l,s,c;if(e=""+e,a="object"==typeof o?o.style+" "+o.variant+" "+o.weight+" "+o.size+"px/"+o.lineHeight+"px "+o.family:o,null==(l=this._textCache[i])&&(l=this._textCache[i]={}),null==(s=l[a])&&(s=l[a]={}),null==(c=s[e])){var f=t("
").html(e).css({position:"absolute","max-width":r,top:-9999}).appendTo(this.getTextLayer(i));"object"==typeof o?f.css({font:a,color:o.color}):"string"==typeof o&&f.addClass(o),c=s[e]={width:f.outerWidth(!0),height:f.outerHeight(!0),element:f,positions:[]},f.detach()}return c},i.prototype.addText=function(t,i,e,o,n,r,a,l,s){var c=this.getTextInfo(t,o,n,r,a),f=c.positions;"center"==l?i-=c.width/2:"right"==l&&(i-=c.width),"middle"==s?e-=c.height/2:"bottom"==s&&(e-=c.height);for(var h,u=0;h=f[u];u++)if(h.x==i&&h.y==e)return void(h.active=!0);h={active:!0,rendered:!1,element:f.length?c.element.clone():c.element,x:i,y:e},f.push(h),h.element.css({top:Math.round(e),left:Math.round(i),"text-align":l})},i.prototype.removeText=function(t,i,e,o,r,a){if(null==o){var l=this._textCache[t];if(null!=l)for(var s in l)if(n.call(l,s)){var c=l[s];for(var f in c)if(n.call(c,f))for(var h=c[f].positions,u=0;d=h[u];u++)d.active=!1}}else for(var d,h=this.getTextInfo(t,o,r,a).positions,u=0;d=h[u];u++)d.x==i&&d.y==e&&(d.active=!1)},t.plot=function(i,o,n){return new e(t(i),o,n,t.plot.plugins)},t.plot.version="0.8.1",t.plot.plugins=[],t.fn.plot=function(i,e){return this.each(function(){t.plot(this,i,e)})}}(jQuery); \ No newline at end of file diff --git a/assets/js/admin/jquery.flot.pie.js b/assets/js/jquery-flot/jquery.flot.pie.js similarity index 100% rename from assets/js/admin/jquery.flot.pie.js rename to assets/js/jquery-flot/jquery.flot.pie.js diff --git a/assets/js/jquery-flot/jquery.flot.pie.min.js b/assets/js/jquery-flot/jquery.flot.pie.min.js new file mode 100644 index 00000000000..5a0959a541a --- /dev/null +++ b/assets/js/jquery-flot/jquery.flot.pie.min.js @@ -0,0 +1 @@ +!function(e){var i=10,s=.95,t={series:{pie:{show:!1,radius:"auto",innerRadius:0,startAngle:1.5,tilt:1,shadow:{left:5,top:15,alpha:.02},offset:{top:0,left:"auto"},stroke:{color:"#fff",width:1},label:{show:"auto",formatter:function(e,i){return"
"+e+"
"+Math.round(i.percent)+"%
"},radius:1,background:{color:null,opacity:0},threshold:0},combine:{threshold:-1,color:null,label:"Other"},highlight:{opacity:.5}}}};e.plot.plugins.push({init:function(r){function a(i,s,r){y||(y=!0,w=i.getCanvas(),k=e(w).parent(),t=i.getOptions(),i.setData(l(i.getData())))}function l(i){for(var s=0,r=0,a=0,l=t.series.pie.combine.color,n=[],o=0;ot.series.pie.combine.threshold)&&n.push({data:[[1,p]],color:i[o].color,label:i[o].label,angle:p*Math.PI*2/s,percent:p/(s/100)})}return a>1&&n.push({data:[[1,r]],color:l,label:t.series.pie.combine.label,angle:r*Math.PI*2/s,percent:r/(s/100)}),n}function n(r,a){function l(){m.clearRect(0,0,n,p),k.children().filter(".pieLabel, .pieLabelBackground").remove()}if(k){var n=r.getPlaceholder().width(),p=r.getPlaceholder().height(),h=k.children().filter(".legend").children().width()||0;m=a,y=!1,M=Math.min(n,p/t.series.pie.tilt)/2,A=p/2+t.series.pie.offset.top,P=n/2,"auto"==t.series.pie.offset.left?t.legend.position.match("w")?P+=h/2:P-=h/2:P+=t.series.pie.offset.left,Pn-M&&(P=n-M);var g=r.getData(),c=0;do{c>0&&(M*=s),c+=1,l(),t.series.pie.tilt<=.8&&function(){var e=t.series.pie.shadow.left,i=t.series.pie.shadow.top,s=t.series.pie.shadow.alpha,r=t.series.pie.radius>1?t.series.pie.radius:M*t.series.pie.radius;if(!(r>=n/2-e||r*t.series.pie.tilt>=p/2-i||r<=10)){m.save(),m.translate(e,i),m.globalAlpha=s,m.fillStyle="#000",m.translate(P,A),m.scale(1,t.series.pie.tilt);for(var a=1;a<=10;a++)m.beginPath(),m.arc(0,0,r,0,2*Math.PI,!1),m.fill(),r-=a;m.restore()}}()}while(!function(){function i(e,i,s){e<=0||isNaN(e)||(s?m.fillStyle=i:(m.strokeStyle=i,m.lineJoin="round"),m.beginPath(),Math.abs(e-2*Math.PI)>1e-9&&m.moveTo(0,0),m.arc(0,0,r,a,a+e/2,!1),m.arc(0,0,r,a+e/2,a+e,!1),m.closePath(),a+=e,s?m.fill():m.stroke())}var s=Math.PI*t.series.pie.startAngle,r=t.series.pie.radius>1?t.series.pie.radius:M*t.series.pie.radius;m.save(),m.translate(P,A),m.scale(1,t.series.pie.tilt),m.save();for(var a=s,l=0;l0){for(m.save(),m.lineWidth=t.series.pie.stroke.width,a=s,l=0;l1?t.series.pie.label.radius:M*t.series.pie.label.radius,a=0;a=100*t.series.pie.label.threshold&&!function(i,s,a){if(0==i.data[0][1])return!0;var l,o=t.legend.labelFormatter,h=t.series.pie.label.formatter;l=o?o(i.label,i):i.label,h&&(l=h(l,i));var g=(s+i.angle+s)/2,c=P+Math.round(Math.cos(g)*r),u=A+Math.round(Math.sin(g)*r)*t.series.pie.tilt,d=""+l+"";k.append(d);var f=k.children("#pieLabel"+a),v=u-f.height()/2,b=c-f.width()/2;if(f.css("top",v),f.css("left",b),0-v>0||0-b>0||p-(v+f.height())<0||n-(b+f.width())<0)return!1;if(0!=t.series.pie.label.background.opacity){var w=t.series.pie.label.background.color;null==w&&(w=i.color);var M="top:"+v+"px;left:"+b+"px;";e("
").css("opacity",t.series.pie.label.background.opacity).insertBefore(f)}return!0}(g[a],i,a))return!1;i+=g[a].angle}return!0}()}()&&c=i&&(l(),k.prepend("
Could not draw pie with labels contained inside canvas
")),r.setSeries&&r.insertLegend&&(r.setSeries(g),r.insertLegend())}}function o(e){if(t.series.pie.innerRadius>0){e.save();var i=t.series.pie.innerRadius>1?t.series.pie.innerRadius:M*t.series.pie.innerRadius;e.globalCompositeOperation="destination-out",e.beginPath(),e.fillStyle=t.series.pie.stroke.color,e.arc(0,0,i,0,2*Math.PI,!1),e.fill(),e.closePath(),e.restore(),e.save(),e.beginPath(),e.strokeStyle=t.series.pie.stroke.color,e.arc(0,0,i,0,2*Math.PI,!1),e.stroke(),e.closePath(),e.restore()}}function p(e,i){for(var s=!1,t=-1,r=e.length,a=r-1;++t1?l.series.pie.radius:M*l.series.pie.radius,o=0;o1?s.series.pie.radius:M*s.series.pie.radius;i.save(),i.translate(P,A),i.scale(1,s.series.pie.tilt);for(var r=0;r1e-9&&i.moveTo(0,0),i.arc(0,0,t,e.startAngle,e.startAngle+e.angle/2,!1),i.arc(0,0,t,e.startAngle+e.angle/2,e.startAngle+e.angle,!1),i.closePath(),i.fill())}(I[r].series);o(i),i.restore()}var w=null,k=null,M=null,P=null,A=null,y=!1,m=null,I=[];r.hooks.processOptions.push(function(e,i){i.series.pie.show&&(i.grid.show=!1,"auto"==i.series.pie.label.show&&(i.legend.show?i.series.pie.label.show=!1:i.series.pie.label.show=!0),"auto"==i.series.pie.radius&&(i.series.pie.label.show?i.series.pie.radius=.75:i.series.pie.radius=1),i.series.pie.tilt>1?i.series.pie.tilt=1:i.series.pie.tilt<0&&(i.series.pie.tilt=0))}),r.hooks.bindEvents.push(function(e,i){var s=e.getOptions();s.series.pie.show&&(s.grid.hoverable&&i.unbind("mousemove").mousemove(g),s.grid.clickable&&i.unbind("click").click(c))}),r.hooks.processDatapoints.push(function(e,i,s,t){e.getOptions().series.pie.show&&a(e)}),r.hooks.drawOverlay.push(function(e,i){e.getOptions().series.pie.show&&b(e,i)}),r.hooks.draw.push(function(e,i){e.getOptions().series.pie.show&&n(e,i)})},options:t,name:"pie",version:"1.1"})}(jQuery); \ No newline at end of file diff --git a/assets/js/admin/jquery.flot.resize.js b/assets/js/jquery-flot/jquery.flot.resize.js old mode 100755 new mode 100644 similarity index 100% rename from assets/js/admin/jquery.flot.resize.js rename to assets/js/jquery-flot/jquery.flot.resize.js diff --git a/assets/js/jquery-flot/jquery.flot.resize.min.js b/assets/js/jquery-flot/jquery.flot.resize.min.js new file mode 100644 index 00000000000..d1230644077 --- /dev/null +++ b/assets/js/jquery-flot/jquery.flot.resize.min.js @@ -0,0 +1 @@ +!function(t,e,i){function n(){h=e[o](function(){r.each(function(){var e=t(this),i=e.width(),n=e.height(),h=t.data(this,u);i===h.w&&n===h.h||e.trigger(a,[h.w=i,h.h=n])}),n()},s[d])}var h,r=t([]),s=t.resize=t.extend(t.resize,{}),o="setTimeout",a="resize",u=a+"-special-event",d="delay",c="throttleWindow";s[d]=250,s[c]=!0,t.event.special[a]={setup:function(){if(!s[c]&&this[o])return!1;var e=t(this);r=r.add(e),t.data(this,u,{w:e.width(),h:e.height()}),1===r.length&&n()},teardown:function(){if(!s[c]&&this[o])return!1;var e=t(this);r=r.not(e),e.removeData(u),r.length||clearTimeout(h)},add:function(e){function n(e,n,r){var s=t(this),o=t.data(this,u);o.w=n!==i?n:s.width(),o.h=r!==i?r:s.height(),h.apply(this,arguments)}if(!s[c]&&this[o])return!1;var h;if(t.isFunction(e))return h=e,n;h=e.handler,e.handler=n}}}(jQuery,this),function(t){var e={};t.plot.plugins.push({init:function(t){function e(){var e=t.getPlaceholder();0!=e.width()&&0!=e.height()&&(t.resize(),t.setupGrid(),t.draw())}t.hooks.bindEvents.push(function(t,i){t.getPlaceholder().resize(e)}),t.hooks.shutdown.push(function(t,i){t.getPlaceholder().unbind("resize",e)})},options:e,name:"resize",version:"1.0"})}(jQuery); \ No newline at end of file diff --git a/assets/js/admin/jquery.flot.stack.js b/assets/js/jquery-flot/jquery.flot.stack.js similarity index 100% rename from assets/js/admin/jquery.flot.stack.js rename to assets/js/jquery-flot/jquery.flot.stack.js diff --git a/assets/js/jquery-flot/jquery.flot.stack.min.js b/assets/js/jquery-flot/jquery.flot.stack.min.js new file mode 100644 index 00000000000..02febf5c2de --- /dev/null +++ b/assets/js/jquery-flot/jquery.flot.stack.min.js @@ -0,0 +1 @@ +!function(s){var n={series:{stack:null}};s.plot.plugins.push({init:function(s){function n(s,n){for(var t=null,i=0;i2&&(d?i.format[2].x:i.format[2].y),D=z&&t.lines.steps,b=!0,j=d?1:0,w=d?0:1,x=0,Q=0;!(x>=g.length);){if(r=m.length,null==g[x]){for(h=0;h=v.length){if(!z)for(h=0;hf){if(z&&x>0&&null!=g[x-c]){for(u=e+(g[x-c+w]-e)*(f-o)/(g[x-c+j]-o),m.push(f),m.push(u+a),h=2;h0&&null!=v[Q-k]&&(p=a+(v[Q-k+w]-a)*(o-f)/(v[Q-k+j]-f)),m[r+w]+=p,x+=c}b=!1,r!=m.length&&y&&(m[r+2]+=p)}if(D&&r!=m.length&&r>0&&null!=m[r]&&m[r]!=m[r-c]&&m[r+1]!=m[r-c+1]){for(h=0;h12?s-12:0==s?12:s;for(var m=0;m=s);++l);var h=m[l][0],f=m[l][1];if("year"==f){if(null!=a.minTickSize&&"year"==a.minTickSize[1])h=Math.floor(a.minTickSize[0]);else{var k=Math.pow(10,Math.floor(Math.log(e.delta/o.year)/Math.LN10)),d=e.delta/o.year/k;h=d<1.5?1:d<3?2:d<7.5?5:10,h*=k}h<1&&(h=1)}e.tickSize=a.tickSize||[h,f];var M=e.tickSize[0];f=e.tickSize[1];var g=M*o[f];"second"==f?r.setSeconds(t(r.getSeconds(),M)):"minute"==f?r.setMinutes(t(r.getMinutes(),M)):"hour"==f?r.setHours(t(r.getHours(),M)):"month"==f?r.setMonth(t(r.getMonth(),M)):"quarter"==f?r.setMonth(3*t(r.getMonth()/3,M)):"year"==f&&r.setFullYear(t(r.getFullYear(),M)),r.setMilliseconds(0),g>=o.minute&&r.setSeconds(0),g>=o.hour&&r.setMinutes(0),g>=o.day&&r.setHours(0),g>=4*o.day&&r.setDate(1),g>=2*o.month&&r.setMonth(t(r.getMonth(),3)),g>=2*o.quarter&&r.setMonth(t(r.getMonth(),6)),g>=o.year&&r.setMonth(0);var y,S=0,z=Number.NaN;do{if(y=z,z=r.getTime(),n.push(z),"month"==f||"quarter"==f)if(M<1){r.setDate(1);var p=r.getTime();r.setMonth(r.getMonth()+("quarter"==f?3:1));var v=r.getTime();r.setTime(z+S*o.hour+(v-p)*M),S=r.getHours(),r.setHours(0)}else r.setMonth(r.getMonth()+M*("quarter"==f?3:1));else"year"==f?r.setFullYear(r.getFullYear()+M):r.setTime(z+g)}while(z -1) { + chr = halfWidth[idx]; + } + value += chr; + } + return value; + }; + + reFormatNumeric = function(e) { + var $target; + $target = $(e.currentTarget); return setTimeout(function() { - var $target, value; - $target = $(e.currentTarget); + var value; value = $target.val(); + value = replaceFullWidthChars(value); + value = value.replace(/\D/g, ''); + return safeVal(value, $target); + }); + }; + + reFormatCardNumber = function(e) { + var $target; + $target = $(e.currentTarget); + return setTimeout(function() { + var value; + value = $target.val(); + value = replaceFullWidthChars(value); value = $.payment.formatCardNumber(value); - return $target.val(value); + return safeVal(value, $target); }); }; @@ -175,10 +250,14 @@ } if (re.test(value)) { e.preventDefault(); - return $target.val(value + ' ' + digit); + return setTimeout(function() { + return $target.val(value + ' ' + digit); + }); } else if (re.test(value + digit)) { e.preventDefault(); - return $target.val(value + digit + ' '); + return setTimeout(function() { + return $target.val(value + digit + ' '); + }); } }; @@ -186,9 +265,6 @@ var $target, value; $target = $(e.currentTarget); value = $target.val(); - if (e.meta) { - return; - } if (e.which !== 8) { return; } @@ -197,13 +273,29 @@ } if (/\d\s$/.test(value)) { e.preventDefault(); - return $target.val(value.replace(/\d\s$/, '')); + return setTimeout(function() { + return $target.val(value.replace(/\d\s$/, '')); + }); } else if (/\s\d?$/.test(value)) { e.preventDefault(); - return $target.val(value.replace(/\s\d?$/, '')); + return setTimeout(function() { + return $target.val(value.replace(/\d$/, '')); + }); } }; + reFormatExpiry = function(e) { + var $target; + $target = $(e.currentTarget); + return setTimeout(function() { + var value; + value = $target.val(); + value = replaceFullWidthChars(value); + value = $.payment.formatExpiry(value); + return safeVal(value, $target); + }); + }; + formatExpiry = function(e) { var $target, digit, val; digit = String.fromCharCode(e.which); @@ -214,10 +306,21 @@ val = $target.val() + digit; if (/^\d$/.test(val) && (val !== '0' && val !== '1')) { e.preventDefault(); - return $target.val("0" + val + " / "); + return setTimeout(function() { + return $target.val("0" + val + " / "); + }); } else if (/^\d\d$/.test(val)) { e.preventDefault(); - return $target.val("" + val + " / "); + return setTimeout(function() { + var m1, m2; + m1 = parseInt(val[0], 10); + m2 = parseInt(val[1], 10); + if (m2 > 2 && m1 !== 0) { + return $target.val("0" + m1 + " / " + m2); + } else { + return $target.val("" + val + " / "); + } + }); } }; @@ -234,10 +337,10 @@ } }; - formatForwardSlash = function(e) { - var $target, slash, val; - slash = String.fromCharCode(e.which); - if (slash !== '/') { + formatForwardSlashAndSpace = function(e) { + var $target, val, which; + which = String.fromCharCode(e.which); + if (!(which === '/' || which === ' ')) { return; } $target = $(e.currentTarget); @@ -249,9 +352,6 @@ formatBackExpiry = function(e) { var $target, value; - if (e.meta) { - return; - } $target = $(e.currentTarget); value = $target.val(); if (e.which !== 8) { @@ -260,15 +360,26 @@ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) { return; } - if (/\d(\s|\/)+$/.test(value)) { + if (/\d\s\/\s$/.test(value)) { e.preventDefault(); - return $target.val(value.replace(/\d(\s|\/)*$/, '')); - } else if (/\s\/\s?\d?$/.test(value)) { - e.preventDefault(); - return $target.val(value.replace(/\s\/\s?\d?$/, '')); + return setTimeout(function() { + return $target.val(value.replace(/\d\s\/\s$/, '')); + }); } }; + reFormatCVC = function(e) { + var $target; + $target = $(e.currentTarget); + return setTimeout(function() { + var value; + value = $target.val(); + value = replaceFullWidthChars(value); + value = value.replace(/\D/g, '').slice(0, 4); + return safeVal(value, $target); + }); + }; + restrictNumeric = function(e) { var input; if (e.metaKey || e.ctrlKey) { @@ -330,6 +441,9 @@ if (!/^\d+$/.test(digit)) { return; } + if (hasTextSelected($target)) { + return; + } val = $target.val() + digit; return val.length <= 4; }; @@ -358,33 +472,44 @@ }; $.payment.fn.formatCardCVC = function() { - this.payment('restrictNumeric'); + this.on('keypress', restrictNumeric); this.on('keypress', restrictCVC); + this.on('paste', reFormatCVC); + this.on('change', reFormatCVC); + this.on('input', reFormatCVC); return this; }; $.payment.fn.formatCardExpiry = function() { - this.payment('restrictNumeric'); + this.on('keypress', restrictNumeric); this.on('keypress', restrictExpiry); this.on('keypress', formatExpiry); - this.on('keypress', formatForwardSlash); + this.on('keypress', formatForwardSlashAndSpace); this.on('keypress', formatForwardExpiry); this.on('keydown', formatBackExpiry); + this.on('change', reFormatExpiry); + this.on('input', reFormatExpiry); return this; }; $.payment.fn.formatCardNumber = function() { - this.payment('restrictNumeric'); + this.on('keypress', restrictNumeric); this.on('keypress', restrictCardNumber); this.on('keypress', formatCardNumber); this.on('keydown', formatBackCardNumber); this.on('keyup', setCardType); this.on('paste', reFormatCardNumber); + this.on('change', reFormatCardNumber); + this.on('input', reFormatCardNumber); + this.on('input', setCardType); return this; }; $.payment.fn.restrictNumeric = function() { this.on('keypress', restrictNumeric); + this.on('paste', reFormatNumeric); + this.on('change', reFormatNumeric); + this.on('input', reFormatNumeric); return this; }; @@ -394,8 +519,7 @@ $.payment.cardExpiryVal = function(value) { var month, prefix, year, _ref; - value = value.replace(/\s/g, ''); - _ref = value.split('/', 2), month = _ref[0], year = _ref[1]; + _ref = value.split(/[\s\/]+/, 2), month = _ref[0], year = _ref[1]; if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) { prefix = (new Date).getFullYear(); prefix = prefix.toString().slice(0, 2); @@ -423,7 +547,7 @@ }; $.payment.validateCardExpiry = function(month, year) { - var currentTime, expiry, prefix, _ref; + var currentTime, expiry, _ref; if (typeof month === 'object' && 'month' in month) { _ref = month, month = _ref.month, year = _ref.year; } @@ -438,13 +562,18 @@ if (!/^\d+$/.test(year)) { return false; } - if (!(parseInt(month, 10) <= 12)) { + if (!((1 <= month && month <= 12))) { return false; } if (year.length === 2) { - prefix = (new Date).getFullYear(); - prefix = prefix.toString().slice(0, 2); - year = prefix + year; + if (year < 70) { + year = "20" + year; + } else { + year = "19" + year; + } + } + if (year.length !== 4) { + return false; } expiry = new Date(year, month); currentTime = new Date; @@ -454,13 +583,14 @@ }; $.payment.validateCardCVC = function(cvc, type) { - var _ref, _ref1; + var card, _ref; cvc = $.trim(cvc); if (!/^\d+$/.test(cvc)) { return false; } - if (type) { - return _ref = cvc.length, __indexOf.call((_ref1 = cardFromType(type)) != null ? _ref1.cvcLength : void 0, _ref) >= 0; + card = cardFromType(type); + if (card != null) { + return _ref = cvc.length, __indexOf.call(card.cvcLength, _ref) >= 0; } else { return cvc.length >= 3 && cvc.length <= 4; } @@ -476,22 +606,51 @@ $.payment.formatCardNumber = function(num) { var card, groups, upperLength, _ref; + num = num.replace(/\D/g, ''); card = cardFromNumber(num); if (!card) { return num; } upperLength = card.length[card.length.length - 1]; - num = num.replace(/\D/g, ''); - num = num.slice(0, +upperLength + 1 || 9e9); + num = num.slice(0, upperLength); if (card.format.global) { return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0; } else { groups = card.format.exec(num); - if (groups != null) { - groups.shift(); + if (groups == null) { + return; } - return groups != null ? groups.join(' ') : void 0; + groups.shift(); + groups = $.grep(groups, function(n) { + return n; + }); + return groups.join(' '); } }; + $.payment.formatExpiry = function(expiry) { + var mon, parts, sep, year; + parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); + if (!parts) { + return ''; + } + mon = parts[1] || ''; + sep = parts[2] || ''; + year = parts[3] || ''; + if (year.length > 0) { + sep = ' / '; + } else if (sep === ' /') { + mon = mon.substring(0, 1); + sep = ''; + } else if (mon.length === 2 || sep.length > 0) { + sep = ' / '; + } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) { + mon = "0" + mon; + sep = ' / '; + } + return mon + sep + year; + }; + }).call(this); + +}); diff --git a/assets/js/jquery-payment/jquery.payment.min.js b/assets/js/jquery-payment/jquery.payment.min.js index 363604d0f8c..648202af215 100644 --- a/assets/js/jquery-payment/jquery.payment.min.js +++ b/assets/js/jquery-payment/jquery.payment.min.js @@ -1,2 +1 @@ -// Generated by CoffeeScript 1.4.0 -(function(){var e,t,n,r,i,s,o,u,a,f,l,c,h,p,d,v,m,g,y,b=[].slice,w=[].indexOf||function(e){for(var t=0,n=this.length;t9&&(t-=9);i+=t}return i%10===0};c=function(e){var t;return e.prop("selectionStart")!=null&&e.prop("selectionStart")!==e.prop("selectionEnd")?!0:(typeof document!="undefined"&&document!==null?(t=document.selection)!=null?typeof t.createRange=="function"?t.createRange().text:void 0:void 0:void 0)?!0:!1};p=function(t){var n=this;return setTimeout(function(){var n,r;n=e(t.currentTarget);r=n.val();r=e.payment.formatCardNumber(r);return n.val(r)})};u=function(n){var r,i,s,o,u,a,f;s=String.fromCharCode(n.which);if(!/^\d+$/.test(s))return;r=e(n.currentTarget);f=r.val();i=t(f+s);o=(f.replace(/\D/g,"")+s).length;a=16;i&&(a=i.length[i.length.length-1]);if(o>=a)return;if(r.prop("selectionStart")!=null&&r.prop("selectionStart")!==f.length)return;i&&i.type==="amex"?u=/^(\d{4}|\d{4}\s\d{6})$/:u=/(?:^|\s)(\d{4})$/;if(u.test(f)){n.preventDefault();return r.val(f+" "+s)}if(u.test(f+s)){n.preventDefault();return r.val(f+s+" ")}};s=function(t){var n,r;n=e(t.currentTarget);r=n.val();if(t.meta)return;if(t.which!==8)return;if(n.prop("selectionStart")!=null&&n.prop("selectionStart")!==r.length)return;if(/\d\s$/.test(r)){t.preventDefault();return n.val(r.replace(/\d\s$/,""))}if(/\s\d?$/.test(r)){t.preventDefault();return n.val(r.replace(/\s\d?$/,""))}};a=function(t){var n,r,i;r=String.fromCharCode(t.which);if(!/^\d+$/.test(r))return;n=e(t.currentTarget);i=n.val()+r;if(/^\d$/.test(i)&&i!=="0"&&i!=="1"){t.preventDefault();return n.val("0"+i+" / ")}if(/^\d\d$/.test(i)){t.preventDefault();return n.val(""+i+" / ")}};f=function(t){var n,r,i;r=String.fromCharCode(t.which);if(!/^\d+$/.test(r))return;n=e(t.currentTarget);i=n.val();if(/^\d\d$/.test(i))return n.val(""+i+" / ")};l=function(t){var n,r,i;r=String.fromCharCode(t.which);if(r!=="/")return;n=e(t.currentTarget);i=n.val();if(/^\d$/.test(i)&&i!=="0")return n.val("0"+i+" / ")};o=function(t){var n,r;if(t.meta)return;n=e(t.currentTarget);r=n.val();if(t.which!==8)return;if(n.prop("selectionStart")!=null&&n.prop("selectionStart")!==r.length)return;if(/\d(\s|\/)+$/.test(r)){t.preventDefault();return n.val(r.replace(/\d(\s|\/)*$/,""))}if(/\s\/\s?\d?$/.test(r)){t.preventDefault();return n.val(r.replace(/\s\/\s?\d?$/,""))}};g=function(e){var t;if(e.metaKey||e.ctrlKey)return!0;if(e.which===32)return!1;if(e.which===0)return!0;if(e.which<33)return!0;t=String.fromCharCode(e.which);return!!/[\d\s]/.test(t)};v=function(n){var r,i,s,o;r=e(n.currentTarget);s=String.fromCharCode(n.which);if(!/^\d+$/.test(s))return;if(c(r))return;o=(r.val()+s).replace(/\D/g,"");i=t(o);return i?o.length<=i.length[i.length.length-1]:o.length<=16};m=function(t){var n,r,i;n=e(t.currentTarget);r=String.fromCharCode(t.which);if(!/^\d+$/.test(r))return;if(c(n))return;i=n.val()+r;i=i.replace(/\D/g,"");if(i.length>6)return!1};d=function(t){var n,r,i;n=e(t.currentTarget);r=String.fromCharCode(t.which);if(!/^\d+$/.test(r))return;i=n.val()+r;return i.length<=4};y=function(t){var n,i,s,o,u;n=e(t.currentTarget);u=n.val();o=e.payment.cardType(u)||"unknown";if(!n.hasClass(o)){i=function(){var e,t,n;n=[];for(e=0,t=r.length;e=0)&&(n.luhn===!1||h(e)):!1};e.payment.validateCardExpiry=function(t,n){var r,i,s,o;typeof t=="object"&&"month"in t&&(o=t,t=o.month,n=o.year);if(!t||!n)return!1;t=e.trim(t);n=e.trim(n);if(!/^\d+$/.test(t))return!1;if(!/^\d+$/.test(n))return!1;if(parseInt(t,10)<=12){if(n.length===2){s=(new Date).getFullYear();s=s.toString().slice(0,2);n=s+n}i=new Date(n,t);r=new Date;i.setMonth(i.getMonth()-1);i.setMonth(i.getMonth()+1,1);return i>r}return!1};e.payment.validateCardCVC=function(t,r){var i,s;t=e.trim(t);return/^\d+$/.test(t)?r?(i=t.length,w.call((s=n(r))!=null?s.cvcLength:void 0,i)>=0):t.length>=3&&t.length<=4:!1};e.payment.cardType=function(e){var n;return e?((n=t(e))!=null?n.type:void 0)||null:null};e.payment.formatCardNumber=function(e){var n,r,i,s;n=t(e);if(!n)return e;i=n.length[n.length.length-1];e=e.replace(/\D/g,"");e=e.slice(0,+i+1||9e9);if(n.format.global)return(s=e.match(n.format))!=null?s.join(" "):void 0;r=n.format.exec(e);r!=null&&r.shift();return r!=null?r.join(" "):void 0}}).call(this); \ No newline at end of file +jQuery(function(t){(function(){var e,n,r,a,i,o,l,u,s,c,h,p,f,g,v,d,m,y,C,T,w,$,D,S=[].slice,k=[].indexOf||function(t){for(var e=0,n=this.length;e9&&(e-=9),a+=e;return a%10==0},h=function(t){var e;return null!=t.prop("selectionStart")&&t.prop("selectionStart")!==t.prop("selectionEnd")||!(null==("undefined"!=typeof document&&null!==document&&null!=(e=document.selection)?e.createRange:void 0)||!document.selection.createRange().text)},$=function(t,e){var n,r,a,i,o;try{r=e.prop("selectionStart")}catch(l){l,r=null}if(i=e.val(),e.val(t),null!==r&&e.is(":focus"))return r===i.length&&(r=t.length),i!==t&&(o=i.slice(r-1,+r+1||9e9),n=t.slice(r-1,+r+1||9e9),a=t[r],/\d/.test(a)&&o===a+" "&&n===" "+a&&(r+=1)),e.prop("selectionStart",r),e.prop("selectionEnd",r)},m=function(t){var e,n,r,a,i,o;for(null==t&&(t=""),"0123456789","0123456789",a="",i=0,o=(e=t.split("")).length;i-1&&(n="0123456789"[r]),a+=n;return a},d=function(e){var n;return n=t(e.currentTarget),setTimeout(function(){var t;return t=n.val(),t=m(t),t=t.replace(/\D/g,""),$(t,n)})},g=function(e){var n;return n=t(e.currentTarget),setTimeout(function(){var e;return e=n.val(),e=m(e),e=t.payment.formatCardNumber(e),$(e,n)})},l=function(n){var r,a,i,o,l,u,s;if(i=String.fromCharCode(n.which),/^\d+$/.test(i)&&(r=t(n.currentTarget),s=r.val(),a=e(s+i),o=(s.replace(/\D/g,"")+i).length,u=16,a&&(u=a.length[a.length.length-1]),!(o>=u||null!=r.prop("selectionStart")&&r.prop("selectionStart")!==s.length)))return(l=a&&"amex"===a.type?/^(\d{4}|\d{4}\s\d{6})$/:/(?:^|\s)(\d{4})$/).test(s)?(n.preventDefault(),setTimeout(function(){return r.val(s+" "+i)})):l.test(s+i)?(n.preventDefault(),setTimeout(function(){return r.val(s+i+" ")})):void 0},i=function(e){var n,r;if(n=t(e.currentTarget),r=n.val(),8===e.which&&(null==n.prop("selectionStart")||n.prop("selectionStart")===r.length))return/\d\s$/.test(r)?(e.preventDefault(),setTimeout(function(){return n.val(r.replace(/\d\s$/,""))})):/\s\d?$/.test(r)?(e.preventDefault(),setTimeout(function(){return n.val(r.replace(/\d$/,""))})):void 0},v=function(e){var n;return n=t(e.currentTarget),setTimeout(function(){var e;return e=n.val(),e=m(e),e=t.payment.formatExpiry(e),$(e,n)})},u=function(e){var n,r,a;if(r=String.fromCharCode(e.which),/^\d+$/.test(r))return n=t(e.currentTarget),a=n.val()+r,/^\d$/.test(a)&&"0"!==a&&"1"!==a?(e.preventDefault(),setTimeout(function(){return n.val("0"+a+" / ")})):/^\d\d$/.test(a)?(e.preventDefault(),setTimeout(function(){var t,e;return t=parseInt(a[0],10),(e=parseInt(a[1],10))>2&&0!==t?n.val("0"+t+" / "+e):n.val(a+" / ")})):void 0},s=function(e){var n,r,a;if(r=String.fromCharCode(e.which),/^\d+$/.test(r))return n=t(e.currentTarget),a=n.val(),/^\d\d$/.test(a)?n.val(a+" / "):void 0},c=function(e){var n,r,a;if("/"===(a=String.fromCharCode(e.which))||" "===a)return n=t(e.currentTarget),r=n.val(),/^\d$/.test(r)&&"0"!==r?n.val("0"+r+" / "):void 0},o=function(e){var n,r;if(n=t(e.currentTarget),r=n.val(),8===e.which&&(null==n.prop("selectionStart")||n.prop("selectionStart")===r.length))return/\d\s\/\s$/.test(r)?(e.preventDefault(),setTimeout(function(){return n.val(r.replace(/\d\s\/\s$/,""))})):void 0},f=function(e){var n;return n=t(e.currentTarget),setTimeout(function(){var t;return t=n.val(),t=m(t),t=t.replace(/\D/g,"").slice(0,4),$(t,n)})},w=function(t){var e;return!(!t.metaKey&&!t.ctrlKey)||32!==t.which&&(0===t.which||(t.which<33||(e=String.fromCharCode(t.which),!!/[\d\s]/.test(e))))},C=function(n){var r,a,i,o;if(r=t(n.currentTarget),i=String.fromCharCode(n.which),/^\d+$/.test(i)&&!h(r))return o=(r.val()+i).replace(/\D/g,""),(a=e(o))?o.length<=a.length[a.length.length-1]:o.length<=16},T=function(e){var n,r,a;if(n=t(e.currentTarget),r=String.fromCharCode(e.which),/^\d+$/.test(r)&&!h(n))return a=n.val()+r,!((a=a.replace(/\D/g,"")).length>6)&&void 0},y=function(e){var n,r;if(n=t(e.currentTarget),r=String.fromCharCode(e.which),/^\d+$/.test(r)&&!h(n))return(n.val()+r).length<=4},D=function(e){var n,a,i,o,l;if(n=t(e.currentTarget),l=n.val(),o=t.payment.cardType(l)||"unknown",!n.hasClass(o))return a=function(){var t,e,n;for(n=[],t=0,e=r.length;t=0&&(!1===n.luhn||p(t))))},t.payment.validateCardExpiry=function(e,n){var r,a,i;return"object"==typeof e&&"month"in e&&(e=(i=e).month,n=i.year),!(!e||!n)&&(e=t.trim(e),n=t.trim(n),!!/^\d+$/.test(e)&&(!!/^\d+$/.test(n)&&(1<=e&&e<=12&&(2===n.length&&(n=n<70?"20"+n:"19"+n),4===n.length&&(a=new Date(n,e),r=new Date,a.setMonth(a.getMonth()-1),a.setMonth(a.getMonth()+1,1),a>r)))))},t.payment.validateCardCVC=function(e,r){var a,i;return e=t.trim(e),!!/^\d+$/.test(e)&&(null!=(a=n(r))?(i=e.length,k.call(a.cvcLength,i)>=0):e.length>=3&&e.length<=4)},t.payment.cardType=function(t){var n;return t?(null!=(n=e(t))?n.type:void 0)||null:null},t.payment.formatCardNumber=function(n){var r,a,i,o;return n=n.replace(/\D/g,""),(r=e(n))?(i=r.length[r.length.length-1],n=n.slice(0,i),r.format.global?null!=(o=n.match(r.format))?o.join(" "):void 0:null!=(a=r.format.exec(n))?(a.shift(),(a=t.grep(a,function(t){return t})).join(" ")):void 0):n},t.payment.formatExpiry=function(t){var e,n,r,a;return(n=t.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/))?(e=n[1]||"",r=n[2]||"",(a=n[3]||"").length>0?r=" / ":" /"===r?(e=e.substring(0,1),r=""):2===e.length||r.length>0?r=" / ":1===e.length&&"0"!==e&&"1"!==e&&(e="0"+e,r=" / "),e+r+a):""}}).call(this)}); \ No newline at end of file diff --git a/assets/js/jquery-qrcode/jquery.qrcode.js b/assets/js/jquery-qrcode/jquery.qrcode.js new file mode 100644 index 00000000000..5b07019bda9 --- /dev/null +++ b/assets/js/jquery-qrcode/jquery.qrcode.js @@ -0,0 +1,1327 @@ +//--------------------------------------------------------------------- +// QRCode for JavaScript +// +// Copyright (c) 2009 Kazuhiko Arase +// +// URL: http://www.d-project.com/ +// +// Licensed under the MIT license: +// http://www.opensource.org/licenses/mit-license.php +// +// The word "QR Code" is registered trademark of +// DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/faqpatent-e.html +// +//--------------------------------------------------------------------- + +//--------------------------------------------------------------------- +// QR8bitByte +//--------------------------------------------------------------------- + +function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE; + this.data = data; +} + +QR8bitByte.prototype = { + + getLength : function(buffer) { + return this.data.length; + }, + + write : function(buffer) { + for (var i = 0; i < this.data.length; i++) { + // not JIS ... + buffer.put(this.data.charCodeAt(i), 8); + } + } +}; + +//--------------------------------------------------------------------- +// QRCode +//--------------------------------------------------------------------- + +function QRCode(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = new Array(); +} + +QRCode.prototype = { + + addData : function(data) { + var newData = new QR8bitByte(data); + this.dataList.push(newData); + this.dataCache = null; + }, + + isDark : function(row, col) { + if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) { + throw new Error(row + "," + col); + } + return this.modules[row][col]; + }, + + getModuleCount : function() { + return this.moduleCount; + }, + + make : function() { + // Calculate automatically typeNumber if provided is < 1 + if (this.typeNumber < 1 ){ + var typeNumber = 1; + for (typeNumber = 1; typeNumber < 40; typeNumber++) { + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, this.errorCorrectLevel); + + var buffer = new QRBitBuffer(); + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount; + } + + for (var i = 0; i < this.dataList.length; i++) { + var data = this.dataList[i]; + buffer.put(data.mode, 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) ); + data.write(buffer); + } + if (buffer.getLengthInBits() <= totalDataCount * 8) + break; + } + this.typeNumber = typeNumber; + } + this.makeImpl(false, this.getBestMaskPattern() ); + }, + + makeImpl : function(test, maskPattern) { + + this.moduleCount = this.typeNumber * 4 + 17; + this.modules = new Array(this.moduleCount); + + for (var row = 0; row < this.moduleCount; row++) { + + this.modules[row] = new Array(this.moduleCount); + + for (var col = 0; col < this.moduleCount; col++) { + this.modules[row][col] = null;//(col + row) % 3; + } + } + + this.setupPositionProbePattern(0, 0); + this.setupPositionProbePattern(this.moduleCount - 7, 0); + this.setupPositionProbePattern(0, this.moduleCount - 7); + this.setupPositionAdjustPattern(); + this.setupTimingPattern(); + this.setupTypeInfo(test, maskPattern); + + if (this.typeNumber >= 7) { + this.setupTypeNumber(test); + } + + if (this.dataCache == null) { + this.dataCache = QRCode.createData(this.typeNumber, this.errorCorrectLevel, this.dataList); + } + + this.mapData(this.dataCache, maskPattern); + }, + + setupPositionProbePattern : function(row, col) { + + for (var r = -1; r <= 7; r++) { + + if (row + r <= -1 || this.moduleCount <= row + r) continue; + + for (var c = -1; c <= 7; c++) { + + if (col + c <= -1 || this.moduleCount <= col + c) continue; + + if ( (0 <= r && r <= 6 && (c == 0 || c == 6) ) + || (0 <= c && c <= 6 && (r == 0 || r == 6) ) + || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) { + this.modules[row + r][col + c] = true; + } else { + this.modules[row + r][col + c] = false; + } + } + } + }, + + getBestMaskPattern : function() { + + var minLostPoint = 0; + var pattern = 0; + + for (var i = 0; i < 8; i++) { + + this.makeImpl(true, i); + + var lostPoint = QRUtil.getLostPoint(this); + + if (i == 0 || minLostPoint > lostPoint) { + minLostPoint = lostPoint; + pattern = i; + } + } + + return pattern; + }, + + createMovieClip : function(target_mc, instance_name, depth) { + + var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth); + var cs = 1; + + this.make(); + + for (var row = 0; row < this.modules.length; row++) { + + var y = row * cs; + + for (var col = 0; col < this.modules[row].length; col++) { + + var x = col * cs; + var dark = this.modules[row][col]; + + if (dark) { + qr_mc.beginFill(0, 100); + qr_mc.moveTo(x, y); + qr_mc.lineTo(x + cs, y); + qr_mc.lineTo(x + cs, y + cs); + qr_mc.lineTo(x, y + cs); + qr_mc.endFill(); + } + } + } + + return qr_mc; + }, + + setupTimingPattern : function() { + + for (var r = 8; r < this.moduleCount - 8; r++) { + if (this.modules[r][6] != null) { + continue; + } + this.modules[r][6] = (r % 2 == 0); + } + + for (var c = 8; c < this.moduleCount - 8; c++) { + if (this.modules[6][c] != null) { + continue; + } + this.modules[6][c] = (c % 2 == 0); + } + }, + + setupPositionAdjustPattern : function() { + + var pos = QRUtil.getPatternPosition(this.typeNumber); + + for (var i = 0; i < pos.length; i++) { + + for (var j = 0; j < pos.length; j++) { + + var row = pos[i]; + var col = pos[j]; + + if (this.modules[row][col] != null) { + continue; + } + + for (var r = -2; r <= 2; r++) { + + for (var c = -2; c <= 2; c++) { + + if (r == -2 || r == 2 || c == -2 || c == 2 + || (r == 0 && c == 0) ) { + this.modules[row + r][col + c] = true; + } else { + this.modules[row + r][col + c] = false; + } + } + } + } + } + }, + + setupTypeNumber : function(test) { + + var bits = QRUtil.getBCHTypeNumber(this.typeNumber); + + for (var i = 0; i < 18; i++) { + var mod = (!test && ( (bits >> i) & 1) == 1); + this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod; + } + + for (var i = 0; i < 18; i++) { + var mod = (!test && ( (bits >> i) & 1) == 1); + this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; + } + }, + + setupTypeInfo : function(test, maskPattern) { + + var data = (this.errorCorrectLevel << 3) | maskPattern; + var bits = QRUtil.getBCHTypeInfo(data); + + // vertical + for (var i = 0; i < 15; i++) { + + var mod = (!test && ( (bits >> i) & 1) == 1); + + if (i < 6) { + this.modules[i][8] = mod; + } else if (i < 8) { + this.modules[i + 1][8] = mod; + } else { + this.modules[this.moduleCount - 15 + i][8] = mod; + } + } + + // horizontal + for (var i = 0; i < 15; i++) { + + var mod = (!test && ( (bits >> i) & 1) == 1); + + if (i < 8) { + this.modules[8][this.moduleCount - i - 1] = mod; + } else if (i < 9) { + this.modules[8][15 - i - 1 + 1] = mod; + } else { + this.modules[8][15 - i - 1] = mod; + } + } + + // fixed module + this.modules[this.moduleCount - 8][8] = (!test); + + }, + + mapData : function(data, maskPattern) { + + var inc = -1; + var row = this.moduleCount - 1; + var bitIndex = 7; + var byteIndex = 0; + + for (var col = this.moduleCount - 1; col > 0; col -= 2) { + + if (col == 6) col--; + + while (true) { + + for (var c = 0; c < 2; c++) { + + if (this.modules[row][col - c] == null) { + + var dark = false; + + if (byteIndex < data.length) { + dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1); + } + + var mask = QRUtil.getMask(maskPattern, row, col - c); + + if (mask) { + dark = !dark; + } + + this.modules[row][col - c] = dark; + bitIndex--; + + if (bitIndex == -1) { + byteIndex++; + bitIndex = 7; + } + } + } + + row += inc; + + if (row < 0 || this.moduleCount <= row) { + row -= inc; + inc = -inc; + break; + } + } + } + + } + +}; + +QRCode.PAD0 = 0xEC; +QRCode.PAD1 = 0x11; + +QRCode.createData = function(typeNumber, errorCorrectLevel, dataList) { + + var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel); + + var buffer = new QRBitBuffer(); + + for (var i = 0; i < dataList.length; i++) { + var data = dataList[i]; + buffer.put(data.mode, 4); + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) ); + data.write(buffer); + } + + // calc num max data. + var totalDataCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalDataCount += rsBlocks[i].dataCount; + } + + if (buffer.getLengthInBits() > totalDataCount * 8) { + throw new Error("code length overflow. (" + + buffer.getLengthInBits() + + ">" + + totalDataCount * 8 + + ")"); + } + + // end code + if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { + buffer.put(0, 4); + } + + // padding + while (buffer.getLengthInBits() % 8 != 0) { + buffer.putBit(false); + } + + // padding + while (true) { + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(QRCode.PAD0, 8); + + if (buffer.getLengthInBits() >= totalDataCount * 8) { + break; + } + buffer.put(QRCode.PAD1, 8); + } + + return QRCode.createBytes(buffer, rsBlocks); +} + +QRCode.createBytes = function(buffer, rsBlocks) { + + var offset = 0; + + var maxDcCount = 0; + var maxEcCount = 0; + + var dcdata = new Array(rsBlocks.length); + var ecdata = new Array(rsBlocks.length); + + for (var r = 0; r < rsBlocks.length; r++) { + + var dcCount = rsBlocks[r].dataCount; + var ecCount = rsBlocks[r].totalCount - dcCount; + + maxDcCount = Math.max(maxDcCount, dcCount); + maxEcCount = Math.max(maxEcCount, ecCount); + + dcdata[r] = new Array(dcCount); + + for (var i = 0; i < dcdata[r].length; i++) { + dcdata[r][i] = 0xff & buffer.buffer[i + offset]; + } + offset += dcCount; + + var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); + var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1); + + var modPoly = rawPoly.mod(rsPoly); + ecdata[r] = new Array(rsPoly.getLength() - 1); + for (var i = 0; i < ecdata[r].length; i++) { + var modIndex = i + modPoly.getLength() - ecdata[r].length; + ecdata[r][i] = (modIndex >= 0)? modPoly.get(modIndex) : 0; + } + + } + + var totalCodeCount = 0; + for (var i = 0; i < rsBlocks.length; i++) { + totalCodeCount += rsBlocks[i].totalCount; + } + + var data = new Array(totalCodeCount); + var index = 0; + + for (var i = 0; i < maxDcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < dcdata[r].length) { + data[index++] = dcdata[r][i]; + } + } + } + + for (var i = 0; i < maxEcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < ecdata[r].length) { + data[index++] = ecdata[r][i]; + } + } + } + + return data; + +} + +//--------------------------------------------------------------------- +// QRMode +//--------------------------------------------------------------------- + +var QRMode = { + MODE_NUMBER : 1 << 0, + MODE_ALPHA_NUM : 1 << 1, + MODE_8BIT_BYTE : 1 << 2, + MODE_KANJI : 1 << 3 +}; + +//--------------------------------------------------------------------- +// QRErrorCorrectLevel +//--------------------------------------------------------------------- + +var QRErrorCorrectLevel = { + L : 1, + M : 0, + Q : 3, + H : 2 +}; + +//--------------------------------------------------------------------- +// QRMaskPattern +//--------------------------------------------------------------------- + +var QRMaskPattern = { + PATTERN000 : 0, + PATTERN001 : 1, + PATTERN010 : 2, + PATTERN011 : 3, + PATTERN100 : 4, + PATTERN101 : 5, + PATTERN110 : 6, + PATTERN111 : 7 +}; + +//--------------------------------------------------------------------- +// QRUtil +//--------------------------------------------------------------------- + +var QRUtil = { + + PATTERN_POSITION_TABLE : [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ], + + G15 : (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), + G18 : (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), + G15_MASK : (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1), + + getBCHTypeInfo : function(data) { + var d = data << 10; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { + d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) ) ); + } + return ( (data << 10) | d) ^ QRUtil.G15_MASK; + }, + + getBCHTypeNumber : function(data) { + var d = data << 12; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { + d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) ) ); + } + return (data << 12) | d; + }, + + getBCHDigit : function(data) { + + var digit = 0; + + while (data != 0) { + digit++; + data >>>= 1; + } + + return digit; + }, + + getPatternPosition : function(typeNumber) { + return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; + }, + + getMask : function(maskPattern, i, j) { + + switch (maskPattern) { + + case QRMaskPattern.PATTERN000 : return (i + j) % 2 == 0; + case QRMaskPattern.PATTERN001 : return i % 2 == 0; + case QRMaskPattern.PATTERN010 : return j % 3 == 0; + case QRMaskPattern.PATTERN011 : return (i + j) % 3 == 0; + case QRMaskPattern.PATTERN100 : return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; + case QRMaskPattern.PATTERN101 : return (i * j) % 2 + (i * j) % 3 == 0; + case QRMaskPattern.PATTERN110 : return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; + case QRMaskPattern.PATTERN111 : return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; + + default : + throw new Error("bad maskPattern:" + maskPattern); + } + }, + + getErrorCorrectPolynomial : function(errorCorrectLength) { + + var a = new QRPolynomial([1], 0); + + for (var i = 0; i < errorCorrectLength; i++) { + a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0) ); + } + + return a; + }, + + getLengthInBits : function(mode, type) { + + if (1 <= type && type < 10) { + + // 1 - 9 + + switch(mode) { + case QRMode.MODE_NUMBER : return 10; + case QRMode.MODE_ALPHA_NUM : return 9; + case QRMode.MODE_8BIT_BYTE : return 8; + case QRMode.MODE_KANJI : return 8; + default : + throw new Error("mode:" + mode); + } + + } else if (type < 27) { + + // 10 - 26 + + switch(mode) { + case QRMode.MODE_NUMBER : return 12; + case QRMode.MODE_ALPHA_NUM : return 11; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 10; + default : + throw new Error("mode:" + mode); + } + + } else if (type < 41) { + + // 27 - 40 + + switch(mode) { + case QRMode.MODE_NUMBER : return 14; + case QRMode.MODE_ALPHA_NUM : return 13; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 12; + default : + throw new Error("mode:" + mode); + } + + } else { + throw new Error("type:" + type); + } + }, + + getLostPoint : function(qrCode) { + + var moduleCount = qrCode.getModuleCount(); + + var lostPoint = 0; + + // LEVEL1 + + for (var row = 0; row < moduleCount; row++) { + + for (var col = 0; col < moduleCount; col++) { + + var sameCount = 0; + var dark = qrCode.isDark(row, col); + + for (var r = -1; r <= 1; r++) { + + if (row + r < 0 || moduleCount <= row + r) { + continue; + } + + for (var c = -1; c <= 1; c++) { + + if (col + c < 0 || moduleCount <= col + c) { + continue; + } + + if (r == 0 && c == 0) { + continue; + } + + if (dark == qrCode.isDark(row + r, col + c) ) { + sameCount++; + } + } + } + + if (sameCount > 5) { + lostPoint += (3 + sameCount - 5); + } + } + } + + // LEVEL2 + + for (var row = 0; row < moduleCount - 1; row++) { + for (var col = 0; col < moduleCount - 1; col++) { + var count = 0; + if (qrCode.isDark(row, col ) ) count++; + if (qrCode.isDark(row + 1, col ) ) count++; + if (qrCode.isDark(row, col + 1) ) count++; + if (qrCode.isDark(row + 1, col + 1) ) count++; + if (count == 0 || count == 4) { + lostPoint += 3; + } + } + } + + // LEVEL3 + + for (var row = 0; row < moduleCount; row++) { + for (var col = 0; col < moduleCount - 6; col++) { + if (qrCode.isDark(row, col) + && !qrCode.isDark(row, col + 1) + && qrCode.isDark(row, col + 2) + && qrCode.isDark(row, col + 3) + && qrCode.isDark(row, col + 4) + && !qrCode.isDark(row, col + 5) + && qrCode.isDark(row, col + 6) ) { + lostPoint += 40; + } + } + } + + for (var col = 0; col < moduleCount; col++) { + for (var row = 0; row < moduleCount - 6; row++) { + if (qrCode.isDark(row, col) + && !qrCode.isDark(row + 1, col) + && qrCode.isDark(row + 2, col) + && qrCode.isDark(row + 3, col) + && qrCode.isDark(row + 4, col) + && !qrCode.isDark(row + 5, col) + && qrCode.isDark(row + 6, col) ) { + lostPoint += 40; + } + } + } + + // LEVEL4 + + var darkCount = 0; + + for (var col = 0; col < moduleCount; col++) { + for (var row = 0; row < moduleCount; row++) { + if (qrCode.isDark(row, col) ) { + darkCount++; + } + } + } + + var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; + lostPoint += ratio * 10; + + return lostPoint; + } + +}; + + +//--------------------------------------------------------------------- +// QRMath +//--------------------------------------------------------------------- + +var QRMath = { + + glog : function(n) { + + if (n < 1) { + throw new Error("glog(" + n + ")"); + } + + return QRMath.LOG_TABLE[n]; + }, + + gexp : function(n) { + + while (n < 0) { + n += 255; + } + + while (n >= 256) { + n -= 255; + } + + return QRMath.EXP_TABLE[n]; + }, + + EXP_TABLE : new Array(256), + + LOG_TABLE : new Array(256) + +}; + +for (var i = 0; i < 8; i++) { + QRMath.EXP_TABLE[i] = 1 << i; +} +for (var i = 8; i < 256; i++) { + QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] + ^ QRMath.EXP_TABLE[i - 5] + ^ QRMath.EXP_TABLE[i - 6] + ^ QRMath.EXP_TABLE[i - 8]; +} +for (var i = 0; i < 255; i++) { + QRMath.LOG_TABLE[QRMath.EXP_TABLE[i] ] = i; +} + +//--------------------------------------------------------------------- +// QRPolynomial +//--------------------------------------------------------------------- + +function QRPolynomial(num, shift) { + + if (num.length == undefined) { + throw new Error(num.length + "/" + shift); + } + + var offset = 0; + + while (offset < num.length && num[offset] == 0) { + offset++; + } + + this.num = new Array(num.length - offset + shift); + for (var i = 0; i < num.length - offset; i++) { + this.num[i] = num[i + offset]; + } +} + +QRPolynomial.prototype = { + + get : function(index) { + return this.num[index]; + }, + + getLength : function() { + return this.num.length; + }, + + multiply : function(e) { + + var num = new Array(this.getLength() + e.getLength() - 1); + + for (var i = 0; i < this.getLength(); i++) { + for (var j = 0; j < e.getLength(); j++) { + num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i) ) + QRMath.glog(e.get(j) ) ); + } + } + + return new QRPolynomial(num, 0); + }, + + mod : function(e) { + + if (this.getLength() - e.getLength() < 0) { + return this; + } + + var ratio = QRMath.glog(this.get(0) ) - QRMath.glog(e.get(0) ); + + var num = new Array(this.getLength() ); + + for (var i = 0; i < this.getLength(); i++) { + num[i] = this.get(i); + } + + for (var i = 0; i < e.getLength(); i++) { + num[i] ^= QRMath.gexp(QRMath.glog(e.get(i) ) + ratio); + } + + // recursive call + return new QRPolynomial(num, 0).mod(e); + } +}; + +//--------------------------------------------------------------------- +// QRRSBlock +//--------------------------------------------------------------------- + +function QRRSBlock(totalCount, dataCount) { + this.totalCount = totalCount; + this.dataCount = dataCount; +} + +QRRSBlock.RS_BLOCK_TABLE = [ + + // L + // M + // Q + // H + + // 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + + // 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + + // 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + + // 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + + // 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + + // 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + + // 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + + // 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + + // 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + + // 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + + // 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + + // 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + + // 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + + // 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + + // 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12], + + // 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + + // 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + + // 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + + // 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + + // 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + + // 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + + // 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + + // 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + + // 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + + // 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + + // 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + + // 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + + // 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + + // 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + + // 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + + // 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + + // 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + + // 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + + // 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + + // 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + + // 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + + // 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + + // 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + + // 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + + // 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] +]; + +QRRSBlock.getRSBlocks = function(typeNumber, errorCorrectLevel) { + + var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); + + if (rsBlock == undefined) { + throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel); + } + + var length = rsBlock.length / 3; + + var list = new Array(); + + for (var i = 0; i < length; i++) { + + var count = rsBlock[i * 3 + 0]; + var totalCount = rsBlock[i * 3 + 1]; + var dataCount = rsBlock[i * 3 + 2]; + + for (var j = 0; j < count; j++) { + list.push(new QRRSBlock(totalCount, dataCount) ); + } + } + + return list; +} + +QRRSBlock.getRsBlockTable = function(typeNumber, errorCorrectLevel) { + + switch(errorCorrectLevel) { + case QRErrorCorrectLevel.L : + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; + case QRErrorCorrectLevel.M : + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; + case QRErrorCorrectLevel.Q : + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; + case QRErrorCorrectLevel.H : + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; + default : + return undefined; + } +} + +//--------------------------------------------------------------------- +// QRBitBuffer +//--------------------------------------------------------------------- + +function QRBitBuffer() { + this.buffer = new Array(); + this.length = 0; +} + +QRBitBuffer.prototype = { + + get : function(index) { + var bufIndex = Math.floor(index / 8); + return ( (this.buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1; + }, + + put : function(num, length) { + for (var i = 0; i < length; i++) { + this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1); + } + }, + + getLengthInBits : function() { + return this.length; + }, + + putBit : function(bit) { + + var bufIndex = Math.floor(this.length / 8); + if (this.buffer.length <= bufIndex) { + this.buffer.push(0); + } + + if (bit) { + this.buffer[bufIndex] |= (0x80 >>> (this.length % 8) ); + } + + this.length++; + } +}; + +(function( $ ){ + $.fn.qrcode = function(options) { + // if options is string, + if( typeof options === 'string' ){ + options = { text: options }; + } + + // set default values + // typeNumber < 1 for automatic calculation + options = $.extend( {}, { + render : "canvas", + width : 256, + height : 256, + typeNumber : -1, + correctLevel : QRErrorCorrectLevel.H, + background : "#ffffff", + foreground : "#000000" + }, options); + + var createCanvas = function(){ + // create the qrcode itself + var qrcode = new QRCode(options.typeNumber, options.correctLevel); + qrcode.addData(options.text); + qrcode.make(); + + // create canvas element + var canvas = document.createElement('canvas'); + canvas.width = options.width; + canvas.height = options.height; + var ctx = canvas.getContext('2d'); + + // compute tileW/tileH based on options.width/options.height + var tileW = options.width / qrcode.getModuleCount(); + var tileH = options.height / qrcode.getModuleCount(); + + // draw in the canvas + for( var row = 0; row < qrcode.getModuleCount(); row++ ){ + for( var col = 0; col < qrcode.getModuleCount(); col++ ){ + ctx.fillStyle = qrcode.isDark(row, col) ? options.foreground : options.background; + var w = (Math.ceil((col+1)*tileW) - Math.floor(col*tileW)); + var h = (Math.ceil((row+1)*tileW) - Math.floor(row*tileW)); + ctx.fillRect(Math.round(col*tileW),Math.round(row*tileH), w, h); + } + } + // return just built canvas + return canvas; + } + + // from Jon-Carlos Rivera (https://github.com/imbcmdth) + var createTable = function(){ + // create the qrcode itself + var qrcode = new QRCode(options.typeNumber, options.correctLevel); + qrcode.addData(options.text); + qrcode.make(); + + // create table element + var $table = $('
') + .css("width", options.width+"px") + .css("height", options.height+"px") + .css("border", "0px") + .css("border-collapse", "collapse") + .css('background-color', options.background); + + // compute tileS percentage + var tileW = options.width / qrcode.getModuleCount(); + var tileH = options.height / qrcode.getModuleCount(); + + // draw in the table + for(var row = 0; row < qrcode.getModuleCount(); row++ ){ + var $row = $('').css('height', tileH+"px").appendTo($table); + + for(var col = 0; col < qrcode.getModuleCount(); col++ ){ + $('') + .css('width', tileW+"px") + .css('background-color', qrcode.isDark(row, col) ? options.foreground : options.background) + .appendTo($row); + } + } + // return just built canvas + return $table; + } + + + return this.each(function(){ + var element = options.render == "canvas" ? createCanvas() : createTable(); + $(element).appendTo(this); + }); + }; +})( jQuery ); diff --git a/assets/js/jquery-qrcode/jquery.qrcode.min.js b/assets/js/jquery-qrcode/jquery.qrcode.min.js new file mode 100644 index 00000000000..a6791313d1c --- /dev/null +++ b/assets/js/jquery-qrcode/jquery.qrcode.min.js @@ -0,0 +1 @@ +function QR8bitByte(t){this.mode=QRMode.MODE_8BIT_BYTE,this.data=t}function QRCode(t,e){this.typeNumber=t,this.errorCorrectLevel=e,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=new Array}function QRPolynomial(t,e){if(t.length==undefined)throw new Error(t.length+"/"+e);for(var r=0;r=7&&this.setupTypeNumber(t),null==this.dataCache&&(this.dataCache=QRCode.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,e)},setupPositionProbePattern:function(t,e){for(var r=-1;r<=7;r++)if(!(t+r<=-1||this.moduleCount<=t+r))for(var o=-1;o<=7;o++)e+o<=-1||this.moduleCount<=e+o||(this.modules[t+r][e+o]=0<=r&&r<=6&&(0==o||6==o)||0<=o&&o<=6&&(0==r||6==r)||2<=r&&r<=4&&2<=o&&o<=4)},getBestMaskPattern:function(){for(var t=0,e=0,r=0;r<8;r++){this.makeImpl(!0,r);var o=QRUtil.getLostPoint(this);(0==r||t>o)&&(t=o,e=r)}return e},createMovieClip:function(t,e,r){var o=t.createEmptyMovieClip(e,r);this.make();for(var n=0;n>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(r=0;r<18;r++){var o=!t&&1==(e>>r&1);this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o}},setupTypeInfo:function(t,e){for(var r=this.errorCorrectLevel<<3|e,o=QRUtil.getBCHTypeInfo(r),n=0;n<15;n++){i=!t&&1==(o>>n&1);n<6?this.modules[n][8]=i:n<8?this.modules[n+1][8]=i:this.modules[this.moduleCount-15+n][8]=i}for(n=0;n<15;n++){var i=!t&&1==(o>>n&1);n<8?this.modules[8][this.moduleCount-n-1]=i:n<9?this.modules[8][15-n-1+1]=i:this.modules[8][15-n-1]=i}this.modules[this.moduleCount-8][8]=!t},mapData:function(t,e){for(var r=-1,o=this.moduleCount-1,n=7,i=0,a=this.moduleCount-1;a>0;a-=2)for(6==a&&a--;;){for(var s=0;s<2;s++)if(null==this.modules[o][a-s]){var u=!1;i>>n&1)),QRUtil.getMask(e,o,a-s)&&(u=!u),this.modules[o][a-s]=u,-1==--n&&(i++,n=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}}},QRCode.PAD0=236,QRCode.PAD1=17,QRCode.createData=function(t,e,r){for(var o=QRRSBlock.getRSBlocks(t,e),n=new QRBitBuffer,i=0;i8*s)throw new Error("code length overflow. ("+n.getLengthInBits()+">"+8*s+")");for(n.getLengthInBits()+4<=8*s&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(!1);for(;;){if(n.getLengthInBits()>=8*s)break;if(n.put(QRCode.PAD0,8),n.getLengthInBits()>=8*s)break;n.put(QRCode.PAD1,8)}return QRCode.createBytes(n,o)},QRCode.createBytes=function(t,e){for(var r=0,o=0,n=0,i=new Array(e.length),a=new Array(e.length),s=0;s=0?f.get(g):0}}for(var d=0,R=0;R=0;)e^=QRUtil.G15<=0;)e^=QRUtil.G18<>>=1;return e},getPatternPosition:function(t){return QRUtil.PATTERN_POSITION_TABLE[t-1]},getMask:function(t,e,r){switch(t){case QRMaskPattern.PATTERN000:return(e+r)%2==0;case QRMaskPattern.PATTERN001:return e%2==0;case QRMaskPattern.PATTERN010:return r%3==0;case QRMaskPattern.PATTERN011:return(e+r)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(e/2)+Math.floor(r/3))%2==0;case QRMaskPattern.PATTERN101:return e*r%2+e*r%3==0;case QRMaskPattern.PATTERN110:return(e*r%2+e*r%3)%2==0;case QRMaskPattern.PATTERN111:return(e*r%3+(e+r)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}},getErrorCorrectPolynomial:function(t){for(var e=new QRPolynomial([1],0),r=0;r5&&(r+=3+n-5)}for(o=0;o=256;)t-=255;return QRMath.EXP_TABLE[t]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},i=0;i<8;i++)QRMath.EXP_TABLE[i]=1<>>7-t%8&1)},put:function(t,e){for(var r=0;r>>e-r-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++}},function(t){t.fn.qrcode=function(e){"string"==typeof e&&(e={text:e}),e=t.extend({},{render:"canvas",width:256,height:256,typeNumber:-1,correctLevel:QRErrorCorrectLevel.H,background:"#ffffff",foreground:"#000000"},e);var r=function(){var t=new QRCode(e.typeNumber,e.correctLevel);t.addData(e.text),t.make();var r=document.createElement("canvas");r.width=e.width,r.height=e.height;for(var o=r.getContext("2d"),n=e.width/t.getModuleCount(),i=e.height/t.getModuleCount(),a=0;a").css("width",e.width+"px").css("height",e.height+"px").css("border","0px").css("border-collapse","collapse").css("background-color",e.background),n=e.width/r.getModuleCount(),i=e.height/r.getModuleCount(),a=0;a").css("height",i+"px").appendTo(o),u=0;u").css("width",n+"px").css("background-color",r.isDark(a,u)?e.foreground:e.background).appendTo(s);return o};return this.each(function(){var n="canvas"==e.render?r():o();t(n).appendTo(this)})}}(jQuery); \ No newline at end of file diff --git a/assets/js/jquery-serializejson/jquery.serializejson.js b/assets/js/jquery-serializejson/jquery.serializejson.js new file mode 100644 index 00000000000..e4259ac91e6 --- /dev/null +++ b/assets/js/jquery-serializejson/jquery.serializejson.js @@ -0,0 +1,340 @@ +/*! + SerializeJSON jQuery plugin. + https://github.com/marioizquierdo/jquery.serializeJSON + version 2.8.1 (Dec, 2016) + + Copyright (c) 2012, 2017 Mario Izquierdo + Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. +*/ +(function (factory) { + if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { // Node/CommonJS + var jQuery = require('jquery'); + module.exports = factory(jQuery); + } else { // Browser globals (zepto supported) + factory(window.jQuery || window.Zepto || window.$); // Zepto supported on browsers as well + } + +}(function ($) { + "use strict"; + + // jQuery('form').serializeJSON() + $.fn.serializeJSON = function (options) { + var f, $form, opts, formAsArray, serializedObject, name, value, parsedValue, _obj, nameWithNoType, type, keys, skipFalsy; + f = $.serializeJSON; + $form = this; // NOTE: the set of matched elements is most likely a form, but it could also be a group of inputs + opts = f.setupOpts(options); // calculate values for options {parseNumbers, parseBoolens, parseNulls, ...} with defaults + + // Use native `serializeArray` function to get an array of {name, value} objects. + formAsArray = $form.serializeArray(); + f.readCheckboxUncheckedValues(formAsArray, opts, $form); // add objects to the array from unchecked checkboxes if needed + + // Convert the formAsArray into a serializedObject with nested keys + serializedObject = {}; + $.each(formAsArray, function (i, obj) { + name = obj.name; // original input name + value = obj.value; // input value + _obj = f.extractTypeAndNameWithNoType(name); + nameWithNoType = _obj.nameWithNoType; // input name with no type (i.e. "foo:string" => "foo") + type = _obj.type; // type defined from the input name in :type colon notation + if (!type) type = f.attrFromInputWithName($form, name, 'data-value-type'); + f.validateType(name, type, opts); // make sure that the type is one of the valid types if defined + + if (type !== 'skip') { // ignore inputs with type 'skip' + keys = f.splitInputNameIntoKeysArray(nameWithNoType); + parsedValue = f.parseValue(value, name, type, opts); // convert to string, number, boolean, null or customType + + skipFalsy = !parsedValue && f.shouldSkipFalsy($form, name, nameWithNoType, type, opts); // ignore falsy inputs if specified + if (!skipFalsy) { + f.deepSet(serializedObject, keys, parsedValue, opts); + } + } + }); + return serializedObject; + }; + + // Use $.serializeJSON as namespace for the auxiliar functions + // and to define defaults + $.serializeJSON = { + + defaultOptions: { + checkboxUncheckedValue: undefined, // to include that value for unchecked checkboxes (instead of ignoring them) + + parseNumbers: false, // convert values like "1", "-2.33" to 1, -2.33 + parseBooleans: false, // convert "true", "false" to true, false + parseNulls: false, // convert "null" to null + parseAll: false, // all of the above + parseWithFunction: null, // to use custom parser, a function like: function(val){ return parsed_val; } + + skipFalsyValuesForTypes: [], // skip serialization of falsy values for listed value types + skipFalsyValuesForFields: [], // skip serialization of falsy values for listed field names + + customTypes: {}, // override defaultTypes + defaultTypes: { + "string": function(str) { return String(str); }, + "number": function(str) { return Number(str); }, + "boolean": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1; }, + "null": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1 ? str : null; }, + "array": function(str) { return JSON.parse(str); }, + "object": function(str) { return JSON.parse(str); }, + "auto": function(str) { return $.serializeJSON.parseValue(str, null, null, {parseNumbers: true, parseBooleans: true, parseNulls: true}); }, // try again with something like "parseAll" + "skip": null // skip is a special type that makes it easy to ignore elements + }, + + useIntKeysAsArrayIndex: false // name="foo[2]" value="v" => {foo: [null, null, "v"]}, instead of {foo: ["2": "v"]} + }, + + // Merge option defaults into the options + setupOpts: function(options) { + var opt, validOpts, defaultOptions, optWithDefault, parseAll, f; + f = $.serializeJSON; + + if (options == null) { options = {}; } // options ||= {} + defaultOptions = f.defaultOptions || {}; // defaultOptions + + // Make sure that the user didn't misspell an option + validOpts = ['checkboxUncheckedValue', 'parseNumbers', 'parseBooleans', 'parseNulls', 'parseAll', 'parseWithFunction', 'skipFalsyValuesForTypes', 'skipFalsyValuesForFields', 'customTypes', 'defaultTypes', 'useIntKeysAsArrayIndex']; // re-define because the user may override the defaultOptions + for (opt in options) { + if (validOpts.indexOf(opt) === -1) { + throw new Error("serializeJSON ERROR: invalid option '" + opt + "'. Please use one of " + validOpts.join(', ')); + } + } + + // Helper to get the default value for this option if none is specified by the user + optWithDefault = function(key) { return (options[key] !== false) && (options[key] !== '') && (options[key] || defaultOptions[key]); }; + + // Return computed options (opts to be used in the rest of the script) + parseAll = optWithDefault('parseAll'); + return { + checkboxUncheckedValue: optWithDefault('checkboxUncheckedValue'), + + parseNumbers: parseAll || optWithDefault('parseNumbers'), + parseBooleans: parseAll || optWithDefault('parseBooleans'), + parseNulls: parseAll || optWithDefault('parseNulls'), + parseWithFunction: optWithDefault('parseWithFunction'), + + skipFalsyValuesForTypes: optWithDefault('skipFalsyValuesForTypes'), + skipFalsyValuesForFields: optWithDefault('skipFalsyValuesForFields'), + typeFunctions: $.extend({}, optWithDefault('defaultTypes'), optWithDefault('customTypes')), + + useIntKeysAsArrayIndex: optWithDefault('useIntKeysAsArrayIndex') + }; + }, + + // Given a string, apply the type or the relevant "parse" options, to return the parsed value + parseValue: function(valStr, inputName, type, opts) { + var f, parsedVal; + f = $.serializeJSON; + parsedVal = valStr; // if no parsing is needed, the returned value will be the same + + if (opts.typeFunctions && type && opts.typeFunctions[type]) { // use a type if available + parsedVal = opts.typeFunctions[type](valStr); + } else if (opts.parseNumbers && f.isNumeric(valStr)) { // auto: number + parsedVal = Number(valStr); + } else if (opts.parseBooleans && (valStr === "true" || valStr === "false")) { // auto: boolean + parsedVal = (valStr === "true"); + } else if (opts.parseNulls && valStr == "null") { // auto: null + parsedVal = null; + } + if (opts.parseWithFunction && !type) { // custom parse function (apply after previous parsing options, but not if there's a specific type) + parsedVal = opts.parseWithFunction(parsedVal, inputName); + } + + return parsedVal; + }, + + isObject: function(obj) { return obj === Object(obj); }, // is it an Object? + isUndefined: function(obj) { return obj === void 0; }, // safe check for undefined values + isValidArrayIndex: function(val) { return /^[0-9]+$/.test(String(val)); }, // 1,2,3,4 ... are valid array indexes + isNumeric: function(obj) { return obj - parseFloat(obj) >= 0; }, // taken from jQuery.isNumeric implementation. Not using jQuery.isNumeric to support old jQuery and Zepto versions + + optionKeys: function(obj) { if (Object.keys) { return Object.keys(obj); } else { var key, keys = []; for(key in obj){ keys.push(key); } return keys;} }, // polyfill Object.keys to get option keys in IE<9 + + + // Fill the formAsArray object with values for the unchecked checkbox inputs, + // using the same format as the jquery.serializeArray function. + // The value of the unchecked values is determined from the opts.checkboxUncheckedValue + // and/or the data-unchecked-value attribute of the inputs. + readCheckboxUncheckedValues: function (formAsArray, opts, $form) { + var selector, $uncheckedCheckboxes, $el, uncheckedValue, f, name; + if (opts == null) { opts = {}; } + f = $.serializeJSON; + + selector = 'input[type=checkbox][name]:not(:checked):not([disabled])'; + $uncheckedCheckboxes = $form.find(selector).add($form.filter(selector)); + $uncheckedCheckboxes.each(function (i, el) { + // Check data attr first, then the option + $el = $(el); + uncheckedValue = $el.attr('data-unchecked-value'); + if (uncheckedValue == null) { + uncheckedValue = opts.checkboxUncheckedValue; + } + + // If there's an uncheckedValue, push it into the serialized formAsArray + if (uncheckedValue != null) { + if (el.name && el.name.indexOf("[][") !== -1) { // identify a non-supported + throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+el.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67"); + } + formAsArray.push({name: el.name, value: uncheckedValue}); + } + }); + }, + + // Returns and object with properties {name_without_type, type} from a given name. + // The type is null if none specified. Example: + // "foo" => {nameWithNoType: "foo", type: null} + // "foo:boolean" => {nameWithNoType: "foo", type: "boolean"} + // "foo[bar]:null" => {nameWithNoType: "foo[bar]", type: "null"} + extractTypeAndNameWithNoType: function(name) { + var match; + if (match = name.match(/(.*):([^:]+)$/)) { + return {nameWithNoType: match[1], type: match[2]}; + } else { + return {nameWithNoType: name, type: null}; + } + }, + + + // Check if this input should be skipped when it has a falsy value, + // depending on the options to skip values by name or type, and the data-skip-falsy attribute. + shouldSkipFalsy: function($form, name, nameWithNoType, type, opts) { + var f = $.serializeJSON; + + var skipFromDataAttr = f.attrFromInputWithName($form, name, 'data-skip-falsy'); + if (skipFromDataAttr != null) { + return skipFromDataAttr !== 'false'; // any value is true, except if explicitly using 'false' + } + + var optForFields = opts.skipFalsyValuesForFields; + if (optForFields && (optForFields.indexOf(nameWithNoType) !== -1 || optForFields.indexOf(name) !== -1)) { + return true; + } + + var optForTypes = opts.skipFalsyValuesForTypes; + if (type == null) type = 'string'; // assume fields with no type are targeted as string + if (optForTypes && optForTypes.indexOf(type) !== -1) { + return true + } + + return false; + }, + + // Finds the first input in $form with this name, and get the given attr from it. + // Returns undefined if no input or no attribute was found. + attrFromInputWithName: function($form, name, attrName) { + var escapedName, selector, $input, attrValue; + escapedName = name.replace(/(:|\.|\[|\]|\s)/g,'\\$1'); // every non-standard character need to be escaped by \\ + selector = '[name="' + escapedName + '"]'; + $input = $form.find(selector).add($form.filter(selector)); // NOTE: this returns only the first $input element if multiple are matched with the same name (i.e. an "array[]"). So, arrays with different element types specified through the data-value-type attr is not supported. + return $input.attr(attrName); + }, + + // Raise an error if the type is not recognized. + validateType: function(name, type, opts) { + var validTypes, f; + f = $.serializeJSON; + validTypes = f.optionKeys(opts ? opts.typeFunctions : f.defaultOptions.defaultTypes); + if (!type || validTypes.indexOf(type) !== -1) { + return true; + } else { + throw new Error("serializeJSON ERROR: Invalid type " + type + " found in input name '" + name + "', please use one of " + validTypes.join(', ')); + } + }, + + + // Split the input name in programatically readable keys. + // Examples: + // "foo" => ['foo'] + // "[foo]" => ['foo'] + // "foo[inn][bar]" => ['foo', 'inn', 'bar'] + // "foo[inn[bar]]" => ['foo', 'inn', 'bar'] + // "foo[inn][arr][0]" => ['foo', 'inn', 'arr', '0'] + // "arr[][val]" => ['arr', '', 'val'] + splitInputNameIntoKeysArray: function(nameWithNoType) { + var keys, f; + f = $.serializeJSON; + keys = nameWithNoType.split('['); // split string into array + keys = $.map(keys, function (key) { return key.replace(/\]/g, ''); }); // remove closing brackets + if (keys[0] === '') { keys.shift(); } // ensure no opening bracket ("[foo][inn]" should be same as "foo[inn]") + return keys; + }, + + // Set a value in an object or array, using multiple keys to set in a nested object or array: + // + // deepSet(obj, ['foo'], v) // obj['foo'] = v + // deepSet(obj, ['foo', 'inn'], v) // obj['foo']['inn'] = v // Create the inner obj['foo'] object, if needed + // deepSet(obj, ['foo', 'inn', '123'], v) // obj['foo']['arr']['123'] = v // + // + // deepSet(obj, ['0'], v) // obj['0'] = v + // deepSet(arr, ['0'], v, {useIntKeysAsArrayIndex: true}) // arr[0] = v + // deepSet(arr, [''], v) // arr.push(v) + // deepSet(obj, ['arr', ''], v) // obj['arr'].push(v) + // + // arr = []; + // deepSet(arr, ['', v] // arr => [v] + // deepSet(arr, ['', 'foo'], v) // arr => [v, {foo: v}] + // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}] + // deepSet(arr, ['', 'bar'], v) // arr => [v, {foo: v, bar: v}, {bar: v}] + // + deepSet: function (o, keys, value, opts) { + var key, nextKey, tail, lastIdx, lastVal, f; + if (opts == null) { opts = {}; } + f = $.serializeJSON; + if (f.isUndefined(o)) { throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined"); } + if (!keys || keys.length === 0) { throw new Error("ArgumentError: param 'keys' expected to be an array with least one element"); } + + key = keys[0]; + + // Only one key, then it's not a deepSet, just assign the value. + if (keys.length === 1) { + if (key === '') { + o.push(value); // '' is used to push values into the array (assume o is an array) + } else { + o[key] = value; // other keys can be used as object keys or array indexes + } + + // With more keys is a deepSet. Apply recursively. + } else { + nextKey = keys[1]; + + // '' is used to push values into the array, + // with nextKey, set the value into the same object, in object[nextKey]. + // Covers the case of ['', 'foo'] and ['', 'var'] to push the object {foo, var}, and the case of nested arrays. + if (key === '') { + lastIdx = o.length - 1; // asume o is array + lastVal = o[lastIdx]; + if (f.isObject(lastVal) && (f.isUndefined(lastVal[nextKey]) || keys.length > 2)) { // if nextKey is not present in the last object element, or there are more keys to deep set + key = lastIdx; // then set the new value in the same object element + } else { + key = lastIdx + 1; // otherwise, point to set the next index in the array + } + } + + // '' is used to push values into the array "array[]" + if (nextKey === '') { + if (f.isUndefined(o[key]) || !$.isArray(o[key])) { + o[key] = []; // define (or override) as array to push values + } + } else { + if (opts.useIntKeysAsArrayIndex && f.isValidArrayIndex(nextKey)) { // if 1, 2, 3 ... then use an array, where nextKey is the index + if (f.isUndefined(o[key]) || !$.isArray(o[key])) { + o[key] = []; // define (or override) as array, to insert values using int keys as array indexes + } + } else { // for anything else, use an object, where nextKey is going to be the attribute name + if (f.isUndefined(o[key]) || !f.isObject(o[key])) { + o[key] = {}; // define (or override) as object, to set nested properties + } + } + } + + // Recursively set the inner object + tail = keys.slice(1); + f.deepSet(o[key], tail, value, opts); + } + } + + }; + +})); diff --git a/assets/js/jquery-serializejson/jquery.serializejson.min.js b/assets/js/jquery-serializejson/jquery.serializejson.min.js new file mode 100644 index 00000000000..731cffe739c --- /dev/null +++ b/assets/js/jquery-serializejson/jquery.serializejson.min.js @@ -0,0 +1,10 @@ +/*! + SerializeJSON jQuery plugin. + https://github.com/marioizquierdo/jquery.serializeJSON + version 2.8.1 (Dec, 2016) + + Copyright (c) 2012, 2017 Mario Izquierdo + Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. +*/ +!function(e){if("function"==typeof define&&define.amd)define(["jquery"],e);else if("object"==typeof exports){var n=require("jquery");module.exports=e(n)}else e(window.jQuery||window.Zepto||window.$)}(function(e){"use strict";e.fn.serializeJSON=function(n){var r,s,t,a,i,u,l,o,p,c,d,f,y;return r=e.serializeJSON,s=this,t=r.setupOpts(n),a=s.serializeArray(),r.readCheckboxUncheckedValues(a,t,s),i={},e.each(a,function(e,n){u=n.name,l=n.value,p=r.extractTypeAndNameWithNoType(u),c=p.nameWithNoType,(d=p.type)||(d=r.attrFromInputWithName(s,u,"data-value-type")),r.validateType(u,d,t),"skip"!==d&&(f=r.splitInputNameIntoKeysArray(c),o=r.parseValue(l,u,d,t),(y=!o&&r.shouldSkipFalsy(s,u,c,d,t))||r.deepSet(i,f,o,t))}),i},e.serializeJSON={defaultOptions:{checkboxUncheckedValue:undefined,parseNumbers:!1,parseBooleans:!1,parseNulls:!1,parseAll:!1,parseWithFunction:null,skipFalsyValuesForTypes:[],skipFalsyValuesForFields:[],customTypes:{},defaultTypes:{string:function(e){return String(e)},number:function(e){return Number(e)},"boolean":function(e){return-1===["false","null","undefined","","0"].indexOf(e)},"null":function(e){return-1===["false","null","undefined","","0"].indexOf(e)?e:null},array:function(e){return JSON.parse(e)},object:function(e){return JSON.parse(e)},auto:function(n){return e.serializeJSON.parseValue(n,null,null,{parseNumbers:!0,parseBooleans:!0,parseNulls:!0})},skip:null},useIntKeysAsArrayIndex:!1},setupOpts:function(n){var r,s,t,a,i,u;u=e.serializeJSON,null==n&&(n={}),t=u.defaultOptions||{},s=["checkboxUncheckedValue","parseNumbers","parseBooleans","parseNulls","parseAll","parseWithFunction","skipFalsyValuesForTypes","skipFalsyValuesForFields","customTypes","defaultTypes","useIntKeysAsArrayIndex"];for(r in n)if(-1===s.indexOf(r))throw new Error("serializeJSON ERROR: invalid option '"+r+"'. Please use one of "+s.join(", "));return a=function(e){return!1!==n[e]&&""!==n[e]&&(n[e]||t[e])},i=a("parseAll"),{checkboxUncheckedValue:a("checkboxUncheckedValue"),parseNumbers:i||a("parseNumbers"),parseBooleans:i||a("parseBooleans"),parseNulls:i||a("parseNulls"),parseWithFunction:a("parseWithFunction"),skipFalsyValuesForTypes:a("skipFalsyValuesForTypes"),skipFalsyValuesForFields:a("skipFalsyValuesForFields"),typeFunctions:e.extend({},a("defaultTypes"),a("customTypes")),useIntKeysAsArrayIndex:a("useIntKeysAsArrayIndex")}},parseValue:function(n,r,s,t){var a,i;return a=e.serializeJSON,i=n,t.typeFunctions&&s&&t.typeFunctions[s]?i=t.typeFunctions[s](n):t.parseNumbers&&a.isNumeric(n)?i=Number(n):!t.parseBooleans||"true"!==n&&"false"!==n?t.parseNulls&&"null"==n&&(i=null):i="true"===n,t.parseWithFunction&&!s&&(i=t.parseWithFunction(i,r)),i},isObject:function(e){return e===Object(e)},isUndefined:function(e){return void 0===e},isValidArrayIndex:function(e){return/^[0-9]+$/.test(String(e))},isNumeric:function(e){return e-parseFloat(e)>=0},optionKeys:function(e){if(Object.keys)return Object.keys(e);var n,r=[];for(n in e)r.push(n);return r},readCheckboxUncheckedValues:function(n,r,s){var t,a,i;null==r&&(r={}),e.serializeJSON,t="input[type=checkbox][name]:not(:checked):not([disabled])",s.find(t).add(s.filter(t)).each(function(s,t){if(a=e(t),null==(i=a.attr("data-unchecked-value"))&&(i=r.checkboxUncheckedValue),null!=i){if(t.name&&-1!==t.name.indexOf("[]["))throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+t.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67");n.push({name:t.name,value:i})}})},extractTypeAndNameWithNoType:function(e){var n;return(n=e.match(/(.*):([^:]+)$/))?{nameWithNoType:n[1],type:n[2]}:{nameWithNoType:e,type:null}},shouldSkipFalsy:function(n,r,s,t,a){var i=e.serializeJSON.attrFromInputWithName(n,r,"data-skip-falsy");if(null!=i)return"false"!==i;var u=a.skipFalsyValuesForFields;if(u&&(-1!==u.indexOf(s)||-1!==u.indexOf(r)))return!0;var l=a.skipFalsyValuesForTypes;return null==t&&(t="string"),!(!l||-1===l.indexOf(t))},attrFromInputWithName:function(e,n,r){var s,t;return s=n.replace(/(:|\.|\[|\]|\s)/g,"\\$1"),t='[name="'+s+'"]',e.find(t).add(e.filter(t)).attr(r)},validateType:function(n,r,s){var t,a;if(a=e.serializeJSON,t=a.optionKeys(s?s.typeFunctions:a.defaultOptions.defaultTypes),r&&-1===t.indexOf(r))throw new Error("serializeJSON ERROR: Invalid type "+r+" found in input name '"+n+"', please use one of "+t.join(", "));return!0},splitInputNameIntoKeysArray:function(n){var r;return e.serializeJSON,r=n.split("["),""===(r=e.map(r,function(e){return e.replace(/\]/g,"")}))[0]&&r.shift(),r},deepSet:function(n,r,s,t){var a,i,u,l,o,p;if(null==t&&(t={}),(p=e.serializeJSON).isUndefined(n))throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined");if(!r||0===r.length)throw new Error("ArgumentError: param 'keys' expected to be an array with least one element");a=r[0],1===r.length?""===a?n.push(s):n[a]=s:(i=r[1],""===a&&(o=n[l=n.length-1],a=p.isObject(o)&&(p.isUndefined(o[i])||r.length>2)?l:l+1),""===i?!p.isUndefined(n[a])&&e.isArray(n[a])||(n[a]=[]):t.useIntKeysAsArrayIndex&&p.isValidArrayIndex(i)?!p.isUndefined(n[a])&&e.isArray(n[a])||(n[a]=[]):!p.isUndefined(n[a])&&p.isObject(n[a])||(n[a]={}),u=r.slice(1),p.deepSet(n[a],u,s,t))}}}); \ No newline at end of file diff --git a/assets/js/jquery-tiptip/jquery.tipTip.min.js b/assets/js/jquery-tiptip/jquery.tipTip.min.js index 8de2e72b2ee..2afafe2a606 100644 --- a/assets/js/jquery-tiptip/jquery.tipTip.min.js +++ b/assets/js/jquery-tiptip/jquery.tipTip.min.js @@ -1,20 +1 @@ -/* - * TipTip - * Copyright 2010 Drew Wilson - * www.drewwilson.com - * code.drewwilson.com/entry/tiptip-jquery-plugin - * - * Version 1.3 - Updated: Mar. 23, 2010 - * - * This Plug-In will create a custom tooltip to replace the default - * browser tooltip. It is extremely lightweight and very smart in - * that it detects the edges of the browser window and will make sure - * the tooltip stays within the current window size. As a result the - * tooltip will adjust itself to be displayed above, below, to the left - * or to the right depending on what is necessary to stay within the - * browser window. It is completely customizable as well via CSS. - * - * This TipTip jQuery plug-in is dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - */(function(e){e.fn.tipTip=function(t){var n={activation:"hover",keepAlive:!1,maxWidth:"200px",edgeOffset:3,defaultPosition:"bottom",delay:400,fadeIn:200,fadeOut:200,attribute:"title",content:!1,enter:function(){},exit:function(){}},r=e.extend(n,t);if(e("#tiptip_holder").length<=0){var i=e('
'),s=e('
'),o=e('
');e("body").append(i.html(s).prepend(o.html('
')))}else var i=e("#tiptip_holder"),s=e("#tiptip_content"),o=e("#tiptip_arrow");return this.each(function(){var t=e(this);if(r.content)var n=r.content;else var n=t.attr(r.attribute);if(n!=""){r.content||t.removeAttr(r.attribute);var u=!1;if(r.activation=="hover"){t.hover(function(){a()},function(){r.keepAlive||f()});r.keepAlive&&i.hover(function(){},function(){f()})}else if(r.activation=="focus")t.focus(function(){a()}).blur(function(){f()});else if(r.activation=="click"){t.click(function(){a();return!1}).hover(function(){},function(){r.keepAlive||f()});r.keepAlive&&i.hover(function(){},function(){f()})}function a(){r.enter.call(this);s.html(n);i.hide().removeAttr("class").css("margin","0");o.removeAttr("style");var a=parseInt(t.offset().top),f=parseInt(t.offset().left),l=parseInt(t.outerWidth()),c=parseInt(t.outerHeight()),h=i.outerWidth(),p=i.outerHeight(),d=Math.round((l-h)/2),v=Math.round((c-p)/2),m=Math.round(f+d),g=Math.round(a+c+r.edgeOffset),y="",b="",w=Math.round(h-12)/2;r.defaultPosition=="bottom"?y="_bottom":r.defaultPosition=="top"?y="_top":r.defaultPosition=="left"?y="_left":r.defaultPosition=="right"&&(y="_right");var E=d+fparseInt(e(window).width());if(E&&d<0||y=="_right"&&!S||y=="_left"&&fparseInt(e(window).height()+e(window).scrollTop()),T=a+c-(r.edgeOffset+p+8)<0;if(x||y=="_bottom"&&x||y=="_top"&&!T){y=="_top"||y=="_bottom"?y="_top":y+="_top";b=p;g=Math.round(a-(p+5+r.edgeOffset))}else if(T|(y=="_top"&&T)||y=="_bottom"&&!x){y=="_top"||y=="_bottom"?y="_bottom":y+="_bottom";b=-12;g=Math.round(a+c+r.edgeOffset)}if(y=="_right_top"||y=="_left_top")g+=5;else if(y=="_right_bottom"||y=="_left_bottom")g-=5;if(y=="_left_top"||y=="_left_bottom")m+=5;o.css({"margin-left":w+"px","margin-top":b+"px"});i.css({"margin-left":m+"px","margin-top":g+"px"}).attr("class","tip"+y);u&&clearTimeout(u);u=setTimeout(function(){i.stop(!0,!0).fadeIn(r.fadeIn)},r.delay)}function f(){r.exit.call(this);u&&clearTimeout(u);i.fadeOut(r.fadeOut)}}})}})(jQuery); \ No newline at end of file +!function(t){t.fn.tipTip=function(e){var o={activation:"hover",keepAlive:!1,maxWidth:"200px",edgeOffset:3,defaultPosition:"bottom",delay:400,fadeIn:200,fadeOut:200,attribute:"title",content:!1,enter:function(){},exit:function(){}},i=t.extend(o,e);if(t("#tiptip_holder").length<=0){var n=t('
'),r=t('
'),a=t('
');t("body").append(n.html(r).prepend(a.html('
')))}else var n=t("#tiptip_holder"),r=t("#tiptip_content"),a=t("#tiptip_arrow");return this.each(function(){function e(){i.enter.call(this),r.html(d),n.hide().removeAttr("class").css("margin","0"),a.removeAttr("style");var e=parseInt(f.offset().top),o=parseInt(f.offset().left),p=parseInt(f.outerWidth()),l=parseInt(f.outerHeight()),h=n.outerWidth(),c=n.outerHeight(),s=Math.round((p-h)/2),_=Math.round((l-c)/2),v=Math.round(o+s),m=Math.round(e+l+i.edgeOffset),g="",b="",M=Math.round(h-12)/2;"bottom"==i.defaultPosition?g="_bottom":"top"==i.defaultPosition?g="_top":"left"==i.defaultPosition?g="_left":"right"==i.defaultPosition&&(g="_right");var w=s+oparseInt(t(window).width());w&&s<0||"_right"==g&&!O||"_left"==g&&oparseInt(t(window).height()+t(window).scrollTop()),I=e+l-(i.edgeOffset+c+8)<0;x||"_bottom"==g&&x||"_top"==g&&!I?("_top"==g||"_bottom"==g?g="_top":g+="_top",b=c,m=Math.round(e-(c+5+i.edgeOffset))):(I|("_top"==g&&I)||"_bottom"==g&&!x)&&("_top"==g||"_bottom"==g?g="_bottom":g+="_bottom",b=-12,m=Math.round(e+l+i.edgeOffset)),"_right_top"==g||"_left_top"==g?m+=5:"_right_bottom"!=g&&"_left_bottom"!=g||(m-=5),"_left_top"!=g&&"_left_bottom"!=g||(v+=5),a.css({"margin-left":M+"px","margin-top":b+"px"}),n.css({"margin-left":v+"px","margin-top":m+"px"}).attr("class","tip"+g),u&&clearTimeout(u),u=setTimeout(function(){n.stop(!0,!0).fadeIn(i.fadeIn)},i.delay)}function o(){i.exit.call(this),u&&clearTimeout(u),n.fadeOut(i.fadeOut)}var f=t(this);if(i.content)d=i.content;else var d=f.attr(i.attribute);if(""!=d){i.content||f.removeAttr(i.attribute);var u=!1;"hover"==i.activation?(f.hover(function(){e()},function(){i.keepAlive||o()}),i.keepAlive&&n.hover(function(){},function(){o()})):"focus"==i.activation?f.focus(function(){e()}).blur(function(){o()}):"click"==i.activation&&(f.click(function(){return e(),!1}).hover(function(){},function(){i.keepAlive||o()}),i.keepAlive&&n.hover(function(){},function(){o()}))}})}}(jQuery); \ No newline at end of file diff --git a/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch.js b/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch.js new file mode 100644 index 00000000000..16ce41d1edf --- /dev/null +++ b/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch.js @@ -0,0 +1,180 @@ +/*! + * jQuery UI Touch Punch 0.2.3 + * + * Copyright 2011–2014, Dave Furfero + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Depends: + * jquery.ui.widget.js + * jquery.ui.mouse.js + */ +(function ($) { + + // Detect touch support + $.support.touch = 'ontouchend' in document; + + // Ignore browsers without touch support + if (!$.support.touch) { + return; + } + + var mouseProto = $.ui.mouse.prototype, + _mouseInit = mouseProto._mouseInit, + _mouseDestroy = mouseProto._mouseDestroy, + touchHandled; + + /** + * Simulate a mouse event based on a corresponding touch event + * @param {Object} event A touch event + * @param {String} simulatedType The corresponding mouse event + */ + function simulateMouseEvent (event, simulatedType) { + + // Ignore multi-touch events + if (event.originalEvent.touches.length > 1) { + return; + } + + event.preventDefault(); + + var touch = event.originalEvent.changedTouches[0], + simulatedEvent = document.createEvent('MouseEvents'); + + // Initialize the simulated mouse event using the touch event's coordinates + simulatedEvent.initMouseEvent( + simulatedType, // type + true, // bubbles + true, // cancelable + window, // view + 1, // detail + touch.screenX, // screenX + touch.screenY, // screenY + touch.clientX, // clientX + touch.clientY, // clientY + false, // ctrlKey + false, // altKey + false, // shiftKey + false, // metaKey + 0, // button + null // relatedTarget + ); + + // Dispatch the simulated event to the target element + event.target.dispatchEvent(simulatedEvent); + } + + /** + * Handle the jQuery UI widget's touchstart events + * @param {Object} event The widget element's touchstart event + */ + mouseProto._touchStart = function (event) { + + var self = this; + + // Ignore the event if another widget is already being handled + if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) { + return; + } + + // Set the flag to prevent other widgets from inheriting the touch event + touchHandled = true; + + // Track movement to determine if interaction was a click + self._touchMoved = false; + + // Simulate the mouseover event + simulateMouseEvent(event, 'mouseover'); + + // Simulate the mousemove event + simulateMouseEvent(event, 'mousemove'); + + // Simulate the mousedown event + simulateMouseEvent(event, 'mousedown'); + }; + + /** + * Handle the jQuery UI widget's touchmove events + * @param {Object} event The document's touchmove event + */ + mouseProto._touchMove = function (event) { + + // Ignore event if not handled + if (!touchHandled) { + return; + } + + // Interaction was not a click + this._touchMoved = true; + + // Simulate the mousemove event + simulateMouseEvent(event, 'mousemove'); + }; + + /** + * Handle the jQuery UI widget's touchend events + * @param {Object} event The document's touchend event + */ + mouseProto._touchEnd = function (event) { + + // Ignore event if not handled + if (!touchHandled) { + return; + } + + // Simulate the mouseup event + simulateMouseEvent(event, 'mouseup'); + + // Simulate the mouseout event + simulateMouseEvent(event, 'mouseout'); + + // If the touch interaction did not move, it should trigger a click + if (!this._touchMoved) { + + // Simulate the click event + simulateMouseEvent(event, 'click'); + } + + // Unset the flag to allow other widgets to inherit the touch event + touchHandled = false; + }; + + /** + * A duck punch of the $.ui.mouse _mouseInit method to support touch events. + * This method extends the widget with bound touch event handlers that + * translate touch events to mouse events and pass them to the widget's + * original mouse event handling methods. + */ + mouseProto._mouseInit = function () { + + var self = this; + + // Delegate the touch handlers to the widget's element + self.element.bind({ + touchstart: $.proxy(self, '_touchStart'), + touchmove: $.proxy(self, '_touchMove'), + touchend: $.proxy(self, '_touchEnd') + }); + + // Call the original $.ui.mouse init method + _mouseInit.call(self); + }; + + /** + * Remove the touch event handlers + */ + mouseProto._mouseDestroy = function () { + + var self = this; + + // Delegate the touch handlers to the widget's element + self.element.unbind({ + touchstart: $.proxy(self, '_touchStart'), + touchmove: $.proxy(self, '_touchMove'), + touchend: $.proxy(self, '_touchEnd') + }); + + // Call the original $.ui.mouse destroy method + _mouseDestroy.call(self); + }; + +})(jQuery); \ No newline at end of file diff --git a/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch.min.js b/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch.min.js new file mode 100644 index 00000000000..d4d30a2c37d --- /dev/null +++ b/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch.min.js @@ -0,0 +1,11 @@ +/*! + * jQuery UI Touch Punch 0.2.3 + * + * Copyright 2011–2014, Dave Furfero + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Depends: + * jquery.ui.widget.js + * jquery.ui.mouse.js + */ +!function(o){function t(o,t){if(!(o.originalEvent.touches.length>1)){o.preventDefault();var e=o.originalEvent.changedTouches[0],u=document.createEvent("MouseEvents");u.initMouseEvent(t,!0,!0,window,1,e.screenX,e.screenY,e.clientX,e.clientY,!1,!1,!1,!1,0,null),o.target.dispatchEvent(u)}}if(o.support.touch="ontouchend"in document,o.support.touch){var e,u=o.ui.mouse.prototype,n=u._mouseInit,c=u._mouseDestroy;u._touchStart=function(o){var u=this;!e&&u._mouseCapture(o.originalEvent.changedTouches[0])&&(e=!0,u._touchMoved=!1,t(o,"mouseover"),t(o,"mousemove"),t(o,"mousedown"))},u._touchMove=function(o){e&&(this._touchMoved=!0,t(o,"mousemove"))},u._touchEnd=function(o){e&&(t(o,"mouseup"),t(o,"mouseout"),this._touchMoved||t(o,"click"),e=!1)},u._mouseInit=function(){var t=this;t.element.bind({touchstart:o.proxy(t,"_touchStart"),touchmove:o.proxy(t,"_touchMove"),touchend:o.proxy(t,"_touchEnd")}),n.call(t)},u._mouseDestroy=function(){var t=this;t.element.unbind({touchstart:o.proxy(t,"_touchStart"),touchmove:o.proxy(t,"_touchMove"),touchend:o.proxy(t,"_touchEnd")}),c.call(t)}}}(jQuery); \ No newline at end of file diff --git a/assets/js/js-cookie/js.cookie.js b/assets/js/js-cookie/js.cookie.js new file mode 100644 index 00000000000..c6c39758317 --- /dev/null +++ b/assets/js/js-cookie/js.cookie.js @@ -0,0 +1,165 @@ +/*! + * JavaScript Cookie v2.1.4 + * https://github.com/js-cookie/js-cookie + * + * Copyright 2006, 2015 Klaus Hartl & Fagner Brack + * Released under the MIT license + */ +;(function (factory) { + var registeredInModuleLoader = false; + if (typeof define === 'function' && define.amd) { + define(factory); + registeredInModuleLoader = true; + } + if (typeof exports === 'object') { + module.exports = factory(); + registeredInModuleLoader = true; + } + if (!registeredInModuleLoader) { + var OldCookies = window.Cookies; + var api = window.Cookies = factory(); + api.noConflict = function () { + window.Cookies = OldCookies; + return api; + }; + } +}(function () { + function extend () { + var i = 0; + var result = {}; + for (; i < arguments.length; i++) { + var attributes = arguments[ i ]; + for (var key in attributes) { + result[key] = attributes[key]; + } + } + return result; + } + + function init (converter) { + function api (key, value, attributes) { + var result; + if (typeof document === 'undefined') { + return; + } + + // Write + + if (arguments.length > 1) { + attributes = extend({ + path: '/' + }, api.defaults, attributes); + + if (typeof attributes.expires === 'number') { + var expires = new Date(); + expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); + attributes.expires = expires; + } + + // We're using "expires" because "max-age" is not supported by IE + attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; + + try { + result = JSON.stringify(value); + if (/^[\{\[]/.test(result)) { + value = result; + } + } catch (e) {} + + if (!converter.write) { + value = encodeURIComponent(String(value)) + .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); + } else { + value = converter.write(value, key); + } + + key = encodeURIComponent(String(key)); + key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); + key = key.replace(/[\(\)]/g, escape); + + var stringifiedAttributes = ''; + + for (var attributeName in attributes) { + if (!attributes[attributeName]) { + continue; + } + stringifiedAttributes += '; ' + attributeName; + if (attributes[attributeName] === true) { + continue; + } + stringifiedAttributes += '=' + attributes[attributeName]; + } + return (document.cookie = key + '=' + value + stringifiedAttributes); + } + + // Read + + if (!key) { + result = {}; + } + + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling "get()" + var cookies = document.cookie ? document.cookie.split('; ') : []; + var rdecode = /(%[0-9A-Z]{2})+/g; + var i = 0; + + for (; i < cookies.length; i++) { + var parts = cookies[i].split('='); + var cookie = parts.slice(1).join('='); + + if (cookie.charAt(0) === '"') { + cookie = cookie.slice(1, -1); + } + + try { + var name = parts[0].replace(rdecode, decodeURIComponent); + cookie = converter.read ? + converter.read(cookie, name) : converter(cookie, name) || + cookie.replace(rdecode, decodeURIComponent); + + if (this.json) { + try { + cookie = JSON.parse(cookie); + } catch (e) {} + } + + if (key === name) { + result = cookie; + break; + } + + if (!key) { + result[name] = cookie; + } + } catch (e) {} + } + + return result; + } + + api.set = api; + api.get = function (key) { + return api.call(api, key); + }; + api.getJSON = function () { + return api.apply({ + json: true + }, [].slice.call(arguments)); + }; + api.defaults = {}; + + api.remove = function (key, attributes) { + api(key, '', extend(attributes, { + expires: -1 + })); + }; + + api.withConverter = init; + + return api; + } + + return init(function () {}); +})); diff --git a/assets/js/js-cookie/js.cookie.min.js b/assets/js/js-cookie/js.cookie.min.js new file mode 100644 index 00000000000..ab19f878e02 --- /dev/null +++ b/assets/js/js-cookie/js.cookie.min.js @@ -0,0 +1,8 @@ +/*! + * JavaScript Cookie v2.1.4 + * https://github.com/js-cookie/js-cookie + * + * Copyright 2006, 2015 Klaus Hartl & Fagner Brack + * Released under the MIT license + */ +!function(e){var n=!1;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n){var o=window.Cookies,t=window.Cookies=e();t.noConflict=function(){return window.Cookies=o,t}}}(function(){function e(){for(var e=0,n={};e1){if("number"==typeof(i=e({path:"/"},t.defaults,i)).expires){var a=new Date;a.setMilliseconds(a.getMilliseconds()+864e5*i.expires),i.expires=a}i.expires=i.expires?i.expires.toUTCString():"";try{c=JSON.stringify(r),/^[\{\[]/.test(c)&&(r=c)}catch(m){}r=o.write?o.write(r,n):encodeURIComponent(String(r)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),n=(n=(n=encodeURIComponent(String(n))).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent)).replace(/[\(\)]/g,escape);var f="";for(var s in i)i[s]&&(f+="; "+s,!0!==i[s]&&(f+="="+i[s]));return document.cookie=n+"="+r+f}n||(c={});for(var p=document.cookie?document.cookie.split("; "):[],d=/(%[0-9A-Z]{2})+/g,u=0;u -1 ) { + uiElement.onTap(); + found = true; + + } + } + + if(found) { + if(e.stopPropagation) { + e.stopPropagation(); + } + _blockControlsTap = true; + + // Some versions of Android don't prevent ghost click event + // when preventDefault() was called on touchstart and/or touchend. + // + // This happens on v4.3, 4.2, 4.1, + // older versions strangely work correctly, + // but just in case we add delay on all of them) + var tapDelay = framework.features.isOldAndroid ? 600 : 30; + _blockControlsTapTimeout = setTimeout(function() { + _blockControlsTap = false; + }, tapDelay); + } + + }, + _fitControlsInViewport = function() { + return !pswp.likelyTouchDevice || _options.mouseUsed || screen.width > _options.fitControlsWidth; + }, + _togglePswpClass = function(el, cName, add) { + framework[ (add ? 'add' : 'remove') + 'Class' ](el, 'pswp__' + cName); + }, + + // add class when there is just one item in the gallery + // (by default it hides left/right arrows and 1ofX counter) + _countNumItems = function() { + var hasOneSlide = (_options.getNumItemsFn() === 1); + + if(hasOneSlide !== _galleryHasOneSlide) { + _togglePswpClass(_controls, 'ui--one-slide', hasOneSlide); + _galleryHasOneSlide = hasOneSlide; + } + }, + _toggleShareModalClass = function() { + _togglePswpClass(_shareModal, 'share-modal--hidden', _shareModalHidden); + }, + _toggleShareModal = function() { + + _shareModalHidden = !_shareModalHidden; + + + if(!_shareModalHidden) { + _toggleShareModalClass(); + setTimeout(function() { + if(!_shareModalHidden) { + framework.addClass(_shareModal, 'pswp__share-modal--fade-in'); + } + }, 30); + } else { + framework.removeClass(_shareModal, 'pswp__share-modal--fade-in'); + setTimeout(function() { + if(_shareModalHidden) { + _toggleShareModalClass(); + } + }, 300); + } + + if(!_shareModalHidden) { + _updateShareURLs(); + } + return false; + }, + + _openWindowPopup = function(e) { + e = e || window.event; + var target = e.target || e.srcElement; + + pswp.shout('shareLinkClick', e, target); + + if(!target.href) { + return false; + } + + if( target.hasAttribute('download') ) { + return true; + } + + window.open(target.href, 'pswp_share', 'scrollbars=yes,resizable=yes,toolbar=no,'+ + 'location=yes,width=550,height=420,top=100,left=' + + (window.screen ? Math.round(screen.width / 2 - 275) : 100) ); + + if(!_shareModalHidden) { + _toggleShareModal(); + } + + return false; + }, + _updateShareURLs = function() { + var shareButtonOut = '', + shareButtonData, + shareURL, + image_url, + page_url, + share_text; + + for(var i = 0; i < _options.shareButtons.length; i++) { + shareButtonData = _options.shareButtons[i]; + + image_url = _options.getImageURLForShare(shareButtonData); + page_url = _options.getPageURLForShare(shareButtonData); + share_text = _options.getTextForShare(shareButtonData); + + shareURL = shareButtonData.url.replace('{{url}}', encodeURIComponent(page_url) ) + .replace('{{image_url}}', encodeURIComponent(image_url) ) + .replace('{{raw_image_url}}', image_url ) + .replace('{{text}}', encodeURIComponent(share_text) ); + + shareButtonOut += '' + + shareButtonData.label + ''; + + if(_options.parseShareButtonOut) { + shareButtonOut = _options.parseShareButtonOut(shareButtonData, shareButtonOut); + } + } + _shareModal.children[0].innerHTML = shareButtonOut; + _shareModal.children[0].onclick = _openWindowPopup; + + }, + _hasCloseClass = function(target) { + for(var i = 0; i < _options.closeElClasses.length; i++) { + if( framework.hasClass(target, 'pswp__' + _options.closeElClasses[i]) ) { + return true; + } + } + }, + _idleInterval, + _idleTimer, + _idleIncrement = 0, + _onIdleMouseMove = function() { + clearTimeout(_idleTimer); + _idleIncrement = 0; + if(_isIdle) { + ui.setIdle(false); + } + }, + _onMouseLeaveWindow = function(e) { + e = e ? e : window.event; + var from = e.relatedTarget || e.toElement; + if (!from || from.nodeName === 'HTML') { + clearTimeout(_idleTimer); + _idleTimer = setTimeout(function() { + ui.setIdle(true); + }, _options.timeToIdleOutside); + } + }, + _setupFullscreenAPI = function() { + if(_options.fullscreenEl && !framework.features.isOldAndroid) { + if(!_fullscrenAPI) { + _fullscrenAPI = ui.getFullscreenAPI(); + } + if(_fullscrenAPI) { + framework.bind(document, _fullscrenAPI.eventK, ui.updateFullscreen); + ui.updateFullscreen(); + framework.addClass(pswp.template, 'pswp--supports-fs'); + } else { + framework.removeClass(pswp.template, 'pswp--supports-fs'); + } + } + }, + _setupLoadingIndicator = function() { + // Setup loading indicator + if(_options.preloaderEl) { + + _toggleLoadingIndicator(true); + + _listen('beforeChange', function() { + + clearTimeout(_loadingIndicatorTimeout); + + // display loading indicator with delay + _loadingIndicatorTimeout = setTimeout(function() { + + if(pswp.currItem && pswp.currItem.loading) { + + if( !pswp.allowProgressiveImg() || (pswp.currItem.img && !pswp.currItem.img.naturalWidth) ) { + // show preloader if progressive loading is not enabled, + // or image width is not defined yet (because of slow connection) + _toggleLoadingIndicator(false); + // items-controller.js function allowProgressiveImg + } + + } else { + _toggleLoadingIndicator(true); // hide preloader + } + + }, _options.loadingIndicatorDelay); + + }); + _listen('imageLoadComplete', function(index, item) { + if(pswp.currItem === item) { + _toggleLoadingIndicator(true); + } + }); + + } + }, + _toggleLoadingIndicator = function(hide) { + if( _loadingIndicatorHidden !== hide ) { + _togglePswpClass(_loadingIndicator, 'preloader--active', !hide); + _loadingIndicatorHidden = hide; + } + }, + _applyNavBarGaps = function(item) { + var gap = item.vGap; + + if( _fitControlsInViewport() ) { + + var bars = _options.barsSize; + if(_options.captionEl && bars.bottom === 'auto') { + if(!_fakeCaptionContainer) { + _fakeCaptionContainer = framework.createEl('pswp__caption pswp__caption--fake'); + _fakeCaptionContainer.appendChild( framework.createEl('pswp__caption__center') ); + _controls.insertBefore(_fakeCaptionContainer, _captionContainer); + framework.addClass(_controls, 'pswp__ui--fit'); + } + if( _options.addCaptionHTMLFn(item, _fakeCaptionContainer, true) ) { + + var captionSize = _fakeCaptionContainer.clientHeight; + gap.bottom = parseInt(captionSize,10) || 44; + } else { + gap.bottom = bars.top; // if no caption, set size of bottom gap to size of top + } + } else { + gap.bottom = bars.bottom === 'auto' ? 0 : bars.bottom; + } + + // height of top bar is static, no need to calculate it + gap.top = bars.top; + } else { + gap.top = gap.bottom = 0; + } + }, + _setupIdle = function() { + // Hide controls when mouse is used + if(_options.timeToIdle) { + _listen('mouseUsed', function() { + + framework.bind(document, 'mousemove', _onIdleMouseMove); + framework.bind(document, 'mouseout', _onMouseLeaveWindow); + + _idleInterval = setInterval(function() { + _idleIncrement++; + if(_idleIncrement === 2) { + ui.setIdle(true); + } + }, _options.timeToIdle / 2); + }); + } + }, + _setupHidingControlsDuringGestures = function() { + + // Hide controls on vertical drag + _listen('onVerticalDrag', function(now) { + if(_controlsVisible && now < 0.95) { + ui.hideControls(); + } else if(!_controlsVisible && now >= 0.95) { + ui.showControls(); + } + }); + + // Hide controls when pinching to close + var pinchControlsHidden; + _listen('onPinchClose' , function(now) { + if(_controlsVisible && now < 0.9) { + ui.hideControls(); + pinchControlsHidden = true; + } else if(pinchControlsHidden && !_controlsVisible && now > 0.9) { + ui.showControls(); + } + }); + + _listen('zoomGestureEnded', function() { + pinchControlsHidden = false; + if(pinchControlsHidden && !_controlsVisible) { + ui.showControls(); + } + }); + + }; + + + + var _uiElements = [ + { + name: 'caption', + option: 'captionEl', + onInit: function(el) { + _captionContainer = el; + } + }, + { + name: 'share-modal', + option: 'shareEl', + onInit: function(el) { + _shareModal = el; + }, + onTap: function() { + _toggleShareModal(); + } + }, + { + name: 'button--share', + option: 'shareEl', + onInit: function(el) { + _shareButton = el; + }, + onTap: function() { + _toggleShareModal(); + } + }, + { + name: 'button--zoom', + option: 'zoomEl', + onTap: pswp.toggleDesktopZoom + }, + { + name: 'counter', + option: 'counterEl', + onInit: function(el) { + _indexIndicator = el; + } + }, + { + name: 'button--close', + option: 'closeEl', + onTap: pswp.close + }, + { + name: 'button--arrow--left', + option: 'arrowEl', + onTap: pswp.prev + }, + { + name: 'button--arrow--right', + option: 'arrowEl', + onTap: pswp.next + }, + { + name: 'button--fs', + option: 'fullscreenEl', + onTap: function() { + if(_fullscrenAPI.isFullscreen()) { + _fullscrenAPI.exit(); + } else { + _fullscrenAPI.enter(); + } + } + }, + { + name: 'preloader', + option: 'preloaderEl', + onInit: function(el) { + _loadingIndicator = el; + } + } + + ]; + + var _setupUIElements = function() { + var item, + classAttr, + uiElement; + + var loopThroughChildElements = function(sChildren) { + if(!sChildren) { + return; + } + + var l = sChildren.length; + for(var i = 0; i < l; i++) { + item = sChildren[i]; + classAttr = item.className; + + for(var a = 0; a < _uiElements.length; a++) { + uiElement = _uiElements[a]; + + if(classAttr.indexOf('pswp__' + uiElement.name) > -1 ) { + + if( _options[uiElement.option] ) { // if element is not disabled from options + + framework.removeClass(item, 'pswp__element--disabled'); + if(uiElement.onInit) { + uiElement.onInit(item); + } + + //item.style.display = 'block'; + } else { + framework.addClass(item, 'pswp__element--disabled'); + //item.style.display = 'none'; + } + } + } + } + }; + loopThroughChildElements(_controls.children); + + var topBar = framework.getChildByClass(_controls, 'pswp__top-bar'); + if(topBar) { + loopThroughChildElements( topBar.children ); + } + }; + + + + + ui.init = function() { + + // extend options + framework.extend(pswp.options, _defaultUIOptions, true); + + // create local link for fast access + _options = pswp.options; + + // find pswp__ui element + _controls = framework.getChildByClass(pswp.scrollWrap, 'pswp__ui'); + + // create local link + _listen = pswp.listen; + + + _setupHidingControlsDuringGestures(); + + // update controls when slides change + _listen('beforeChange', ui.update); + + // toggle zoom on double-tap + _listen('doubleTap', function(point) { + var initialZoomLevel = pswp.currItem.initialZoomLevel; + if(pswp.getZoomLevel() !== initialZoomLevel) { + pswp.zoomTo(initialZoomLevel, point, 333); + } else { + pswp.zoomTo(_options.getDoubleTapZoom(false, pswp.currItem), point, 333); + } + }); + + // Allow text selection in caption + _listen('preventDragEvent', function(e, isDown, preventObj) { + var t = e.target || e.srcElement; + if( + t && + t.getAttribute('class') && e.type.indexOf('mouse') > -1 && + ( t.getAttribute('class').indexOf('__caption') > 0 || (/(SMALL|STRONG|EM)/i).test(t.tagName) ) + ) { + preventObj.prevent = false; + } + }); + + // bind events for UI + _listen('bindEvents', function() { + framework.bind(_controls, 'pswpTap click', _onControlsTap); + framework.bind(pswp.scrollWrap, 'pswpTap', ui.onGlobalTap); + + if(!pswp.likelyTouchDevice) { + framework.bind(pswp.scrollWrap, 'mouseover', ui.onMouseOver); + } + }); + + // unbind events for UI + _listen('unbindEvents', function() { + if(!_shareModalHidden) { + _toggleShareModal(); + } + + if(_idleInterval) { + clearInterval(_idleInterval); + } + framework.unbind(document, 'mouseout', _onMouseLeaveWindow); + framework.unbind(document, 'mousemove', _onIdleMouseMove); + framework.unbind(_controls, 'pswpTap click', _onControlsTap); + framework.unbind(pswp.scrollWrap, 'pswpTap', ui.onGlobalTap); + framework.unbind(pswp.scrollWrap, 'mouseover', ui.onMouseOver); + + if(_fullscrenAPI) { + framework.unbind(document, _fullscrenAPI.eventK, ui.updateFullscreen); + if(_fullscrenAPI.isFullscreen()) { + _options.hideAnimationDuration = 0; + _fullscrenAPI.exit(); + } + _fullscrenAPI = null; + } + }); + + + // clean up things when gallery is destroyed + _listen('destroy', function() { + if(_options.captionEl) { + if(_fakeCaptionContainer) { + _controls.removeChild(_fakeCaptionContainer); + } + framework.removeClass(_captionContainer, 'pswp__caption--empty'); + } + + if(_shareModal) { + _shareModal.children[0].onclick = null; + } + framework.removeClass(_controls, 'pswp__ui--over-close'); + framework.addClass( _controls, 'pswp__ui--hidden'); + ui.setIdle(false); + }); + + + if(!_options.showAnimationDuration) { + framework.removeClass( _controls, 'pswp__ui--hidden'); + } + _listen('initialZoomIn', function() { + if(_options.showAnimationDuration) { + framework.removeClass( _controls, 'pswp__ui--hidden'); + } + }); + _listen('initialZoomOut', function() { + framework.addClass( _controls, 'pswp__ui--hidden'); + }); + + _listen('parseVerticalMargin', _applyNavBarGaps); + + _setupUIElements(); + + if(_options.shareEl && _shareButton && _shareModal) { + _shareModalHidden = true; + } + + _countNumItems(); + + _setupIdle(); + + _setupFullscreenAPI(); + + _setupLoadingIndicator(); + }; + + ui.setIdle = function(isIdle) { + _isIdle = isIdle; + _togglePswpClass(_controls, 'ui--idle', isIdle); + }; + + ui.update = function() { + // Don't update UI if it's hidden + if(_controlsVisible && pswp.currItem) { + + ui.updateIndexIndicator(); + + if(_options.captionEl) { + _options.addCaptionHTMLFn(pswp.currItem, _captionContainer); + + _togglePswpClass(_captionContainer, 'caption--empty', !pswp.currItem.title); + } + + _overlayUIUpdated = true; + + } else { + _overlayUIUpdated = false; + } + + if(!_shareModalHidden) { + _toggleShareModal(); + } + + _countNumItems(); + }; + + ui.updateFullscreen = function(e) { + + if(e) { + // some browsers change window scroll position during the fullscreen + // so PhotoSwipe updates it just in case + setTimeout(function() { + pswp.setScrollOffset( 0, framework.getScrollY() ); + }, 50); + } + + // toogle pswp--fs class on root element + framework[ (_fullscrenAPI.isFullscreen() ? 'add' : 'remove') + 'Class' ](pswp.template, 'pswp--fs'); + }; + + ui.updateIndexIndicator = function() { + if(_options.counterEl) { + _indexIndicator.innerHTML = (pswp.getCurrentIndex()+1) + + _options.indexIndicatorSep + + _options.getNumItemsFn(); + } + }; + + ui.onGlobalTap = function(e) { + e = e || window.event; + var target = e.target || e.srcElement; + + if(_blockControlsTap) { + return; + } + + if(e.detail && e.detail.pointerType === 'mouse') { + + // close gallery if clicked outside of the image + if(_hasCloseClass(target)) { + pswp.close(); + return; + } + + if(framework.hasClass(target, 'pswp__img')) { + if(pswp.getZoomLevel() === 1 && pswp.getZoomLevel() <= pswp.currItem.fitRatio) { + if(_options.clickToCloseNonZoomable) { + pswp.close(); + } + } else { + pswp.toggleDesktopZoom(e.detail.releasePoint); + } + } + + } else { + + // tap anywhere (except buttons) to toggle visibility of controls + if(_options.tapToToggleControls) { + if(_controlsVisible) { + ui.hideControls(); + } else { + ui.showControls(); + } + } + + // tap to close gallery + if(_options.tapToClose && (framework.hasClass(target, 'pswp__img') || _hasCloseClass(target)) ) { + pswp.close(); + return; + } + + } + }; + ui.onMouseOver = function(e) { + e = e || window.event; + var target = e.target || e.srcElement; + + // add class when mouse is over an element that should close the gallery + _togglePswpClass(_controls, 'ui--over-close', _hasCloseClass(target)); + }; + + ui.hideControls = function() { + framework.addClass(_controls,'pswp__ui--hidden'); + _controlsVisible = false; + }; + + ui.showControls = function() { + _controlsVisible = true; + if(!_overlayUIUpdated) { + ui.update(); + } + framework.removeClass(_controls,'pswp__ui--hidden'); + }; + + ui.supportsFullscreen = function() { + var d = document; + return !!(d.exitFullscreen || d.mozCancelFullScreen || d.webkitExitFullscreen || d.msExitFullscreen); + }; + + ui.getFullscreenAPI = function() { + var dE = document.documentElement, + api, + tF = 'fullscreenchange'; + + if (dE.requestFullscreen) { + api = { + enterK: 'requestFullscreen', + exitK: 'exitFullscreen', + elementK: 'fullscreenElement', + eventK: tF + }; + + } else if(dE.mozRequestFullScreen ) { + api = { + enterK: 'mozRequestFullScreen', + exitK: 'mozCancelFullScreen', + elementK: 'mozFullScreenElement', + eventK: 'moz' + tF + }; + + + + } else if(dE.webkitRequestFullscreen) { + api = { + enterK: 'webkitRequestFullscreen', + exitK: 'webkitExitFullscreen', + elementK: 'webkitFullscreenElement', + eventK: 'webkit' + tF + }; + + } else if(dE.msRequestFullscreen) { + api = { + enterK: 'msRequestFullscreen', + exitK: 'msExitFullscreen', + elementK: 'msFullscreenElement', + eventK: 'MSFullscreenChange' + }; + } + + if(api) { + api.enter = function() { + // disable close-on-scroll in fullscreen + _initalCloseOnScrollValue = _options.closeOnScroll; + _options.closeOnScroll = false; + + if(this.enterK === 'webkitRequestFullscreen') { + pswp.template[this.enterK]( Element.ALLOW_KEYBOARD_INPUT ); + } else { + return pswp.template[this.enterK](); + } + }; + api.exit = function() { + _options.closeOnScroll = _initalCloseOnScrollValue; + + return document[this.exitK](); + + }; + api.isFullscreen = function() { return document[this.elementK]; }; + } + + return api; + }; + + + +}; +return PhotoSwipeUI_Default; + + +}); diff --git a/assets/js/photoswipe/photoswipe-ui-default.min.js b/assets/js/photoswipe/photoswipe-ui-default.min.js new file mode 100644 index 00000000000..13aa1f80c8c --- /dev/null +++ b/assets/js/photoswipe/photoswipe-ui-default.min.js @@ -0,0 +1,4 @@ +/*! PhotoSwipe Default UI - 4.1.1 - 2015-12-24 +* http://photoswipe.com +* Copyright (c) 2015 Dmitry Semenov; */ +!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():e.PhotoSwipeUI_Default=t()}(this,function(){"use strict";return function(e,t){var n,o,l,r,i,s,a,u,c,p,d,m,f,h,w,g,v,b,_,C=this,T=!1,I=!0,E=!0,F={barsSize:{top:44,bottom:"auto"},closeElClasses:["item","caption","zoom-wrap","ui","top-bar"],timeToIdle:4e3,timeToIdleOutside:1e3,loadingIndicatorDelay:1e3,addCaptionHTMLFn:function(e,t){return e.title?(t.children[0].innerHTML=e.title,!0):(t.children[0].innerHTML="",!1)},closeEl:!0,captionEl:!0,fullscreenEl:!0,zoomEl:!0,shareEl:!0,counterEl:!0,arrowEl:!0,preloaderEl:!0,tapToClose:!1,tapToToggleControls:!0,clickToCloseNonZoomable:!0,shareButtons:[{id:"facebook",label:"Share on Facebook",url:"https://www.facebook.com/sharer/sharer.php?u={{url}}"},{id:"twitter",label:"Tweet",url:"https://twitter.com/intent/tweet?text={{text}}&url={{url}}"},{id:"pinterest",label:"Pin it",url:"http://www.pinterest.com/pin/create/button/?url={{url}}&media={{image_url}}&description={{text}}"},{id:"download",label:"Download image",url:"{{raw_image_url}}",download:!0}],getImageURLForShare:function(){return e.currItem.src||""},getPageURLForShare:function(){return window.location.href},getTextForShare:function(){return e.currItem.title||""},indexIndicatorSep:" / ",fitControlsWidth:1200},x=function(e){if(g)return!0;e=e||window.event,w.timeToIdle&&w.mouseUsed&&!c&&D();for(var n,o,l=(e.target||e.srcElement).getAttribute("class")||"",r=0;r-1&&(n.onTap(),o=!0);if(o){e.stopPropagation&&e.stopPropagation(),g=!0;var i=t.features.isOldAndroid?600:30;v=setTimeout(function(){g=!1},i)}},S=function(){return!e.likelyTouchDevice||w.mouseUsed||screen.width>w.fitControlsWidth},k=function(e,n,o){t[(o?"add":"remove")+"Class"](e,"pswp__"+n)},K=function(){var e=1===w.getNumItemsFn();e!==h&&(k(o,"ui--one-slide",e),h=e)},L=function(){k(a,"share-modal--hidden",E)},O=function(){return(E=!E)?(t.removeClass(a,"pswp__share-modal--fade-in"),setTimeout(function(){E&&L()},300)):(L(),setTimeout(function(){E||t.addClass(a,"pswp__share-modal--fade-in")},30)),E||y(),!1},R=function(t){var n=(t=t||window.event).target||t.srcElement;return e.shout("shareLinkClick",t,n),!(!n.href||!n.hasAttribute("download")&&(window.open(n.href,"pswp_share","scrollbars=yes,resizable=yes,toolbar=no,location=yes,width=550,height=420,top=100,left="+(window.screen?Math.round(screen.width/2-275):100)),E||O(),1))},y=function(){for(var e,t,n,o,l="",r=0;r
-

-

-
-
-
-
- - - - - - - - - - - - +

- + + +
attribute_label ); ?> +
+
+
+
+ + + + + + + + + + + + + - - - - + + + + - - - -
+ attribute_label ); ?> -
|
-
attribute_name ); ?>attribute_type ) ); ?>attribute_orderby ) { - case 'name' : - _e( 'Name', 'woocommerce' ); - break; - case 'id' : - _e( 'Term ID', 'woocommerce' ); - break; - default: - _e( 'Custom ordering', 'woocommerce' ); - break; - } - ?>attribute_name))) : - $terms_array = array(); - $terms = get_terms( wc_attribute_taxonomy_name($tax->attribute_name), 'orderby=name&hide_empty=0' ); - if ($terms) : - foreach ($terms as $term) : - $terms_array[] = $term->name; - endforeach; - echo implode(', ', $terms_array); - else : +
|
+
attribute_name ); ?>attribute_type ) ); ?> attribute_public ? __( '(Public)', 'woocommerce' ) : ''; ?>attribute_orderby ) { + case 'name' : + _e( 'Name', 'woocommerce' ); + break; + case 'name_num' : + _e( 'Name (numeric)', 'woocommerce' ); + break; + case 'id' : + _e( 'Term ID', 'woocommerce' ); + break; + default: + _e( 'Custom ordering', 'woocommerce' ); + break; + } + ?>attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + if ( 'menu_order' === wc_attribute_orderby( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'hide_empty=0&menu_order=ASC' ); + } else { + $terms = get_terms( $taxonomy, 'hide_empty=0&menu_order=false' ); + } + + switch ( $tax->attribute_orderby ) { + case 'name_num' : + usort( $terms, '_wc_get_product_terms_name_num_usort_callback' ); + break; + case 'parent' : + usort( $terms, '_wc_get_product_terms_parent_usort_callback' ); + break; + } + + $terms_string = implode( ', ', wp_list_pluck( $terms, 'name' ) ); + if ( $terms_string ) { + echo $terms_string; + } else { echo ''; - endif; - else : + } + } else { echo ''; - endif; - ?>
-
-
-
-
-
-

-

-
+ } + ?> +
+
+
+
+
+
+
+

+

+ + +
@@ -377,15 +459,31 @@ class WC_Admin_Attributes {
-

+

+
+ +
+ + +

products -> product data -> attributes -> values, Text allows manual entry whereas select allows pre-configured terms in a drop-down list.', 'woocommerce' ); ?>

@@ -393,32 +491,36 @@ class WC_Admin_Attributes {

-

+ + +

- -
-
-
-
-
__( 'WooCommerce endpoints', 'woocommerce' ), + 'type_label' => __( 'WooCommerce endpoint', 'woocommerce' ), + 'type' => 'woocommerce_nav', + 'object' => 'woocommerce_endpoint', + ); + + return $item_types; + } + + /** + * Register account endpoints to customize nav menu items. + * + * @since 3.1.0 + * @param array $items List of nav menu items. + * @param string $type Nav menu type. + * @param string $object Nav menu object. + * @param integer $page Page number. + * @return array + */ + public function register_customize_nav_menu_items( $items = array(), $type = '', $object = '', $page = 0 ) { + if ( 'woocommerce_endpoint' !== $object ) { + return $items; + } + + // Don't allow pagination since all items are loaded at once. + if ( 0 < $page ) { + return $items; + } + + // Get items from account menu. + $endpoints = wc_get_account_menu_items(); + + // Remove dashboard item. + if ( isset( $endpoints['dashboard'] ) ) { + unset( $endpoints['dashboard'] ); + } + + // Include missing lost password. + $endpoints['lost-password'] = __( 'Lost password', 'woocommerce' ); + + $endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints ); + + foreach ( $endpoints as $endpoint => $title ) { + $items[] = array( + 'id' => $endpoint, + 'title' => $title, + 'type_label' => __( 'Custom Link', 'woocommerce' ), + 'url' => esc_url_raw( wc_get_account_endpoint_url( $endpoint ) ), + ); + } + + return $items; + } +} + +endif; + +return new WC_Admin_Customize(); diff --git a/includes/admin/class-wc-admin-dashboard.php b/includes/admin/class-wc-admin-dashboard.php index 67756a7b404..a3defa01527 100644 --- a/includes/admin/class-wc-admin-dashboard.php +++ b/includes/admin/class-wc-admin-dashboard.php @@ -2,18 +2,20 @@ /** * Admin Dashboard * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} -if ( ! class_exists( 'WC_Admin_Dashboard' ) ) : +if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) : /** - * WC_Admin_Dashboard Class + * WC_Admin_Dashboard Class. */ class WC_Admin_Dashboard { @@ -28,46 +30,29 @@ class WC_Admin_Dashboard { } /** - * Init dashboard widgets + * Init dashboard widgets. */ public function init() { - if ( current_user_can( 'publish_shop_orders' ) ) { - wp_add_dashboard_widget( 'woocommerce_dashboard_recent_reviews', __( 'WooCommerce Recent Reviews', 'woocommerce' ), array( $this, 'recent_reviews' ) ); + if ( current_user_can( 'publish_shop_orders' ) && post_type_supports( 'product', 'comments' ) ) { + wp_add_dashboard_widget( 'woocommerce_dashboard_recent_reviews', __( 'WooCommerce recent reviews', 'woocommerce' ), array( $this, 'recent_reviews' ) ); } - - wp_add_dashboard_widget( 'woocommerce_dashboard_status', __( 'WooCommerce Status', 'woocommerce' ), array( $this, 'status_widget' ) ); + wp_add_dashboard_widget( 'woocommerce_dashboard_status', __( 'WooCommerce status', 'woocommerce' ), array( $this, 'status_widget' ) ); } /** - * Show status widget + * Get top seller from DB. + * @return object */ - public function status_widget() { + private function get_top_seller() { global $wpdb; - include_once( 'reports/class-wc-admin-report.php' ); - - $reports = new WC_Admin_Report(); - - // Sales - $query = array(); - $query['fields'] = "SELECT SUM( postmeta.meta_value ) FROM {$wpdb->posts} as posts"; - $query['join'] = "INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id "; - $query['where'] = "WHERE posts.post_type = 'shop_order' "; - $query['where'] .= "AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "' ) "; - $query['where'] .= "AND postmeta.meta_key = '_order_total' "; - $query['where'] .= "AND posts.post_date >= '" . date( 'Y-m-01', current_time( 'timestamp' ) ) . "' "; - $query['where'] .= "AND posts.post_date <= '" . date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) . "' "; - - $sales = $wpdb->get_var( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_sales_query', $query ) ) ); - - // Get top seller $query = array(); $query['fields'] = "SELECT SUM( order_item_meta.meta_value ) as qty, order_item_meta_2.meta_value as product_id FROM {$wpdb->posts} as posts"; $query['join'] = "INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_id "; $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id "; $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_2 ON order_items.order_item_id = order_item_meta_2.order_item_id "; - $query['where'] = "WHERE posts.post_type = 'shop_order' "; + $query['where'] = "WHERE posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' ) "; $query['where'] .= "AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "' ) "; $query['where'] .= "AND order_item_meta.meta_key = '_qty' "; $query['where'] .= "AND order_item_meta_2.meta_key = '_product_id' "; @@ -77,102 +62,206 @@ class WC_Admin_Dashboard { $query['orderby'] = "ORDER BY qty DESC"; $query['limits'] = "LIMIT 1"; - $top_seller = $wpdb->get_row( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_top_seller_query', $query ) ) ); + return $wpdb->get_row( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_top_seller_query', $query ) ) ); + } - // Counts - $counts = (array) wp_count_posts( 'shop_order' ); - $on_hold_count = $counts['wc-on-hold']; - $processing_count = $counts['wc-processing']; + /** + * Get sales report data. + * @return object + */ + private function get_sales_report_data() { + include_once( dirname( __FILE__ ) . '/reports/class-wc-report-sales-by-date.php' ); - // Get products using a query - this is too advanced for get_posts :( - $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); - $nostock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); + $sales_by_date = new WC_Report_Sales_By_Date(); + $sales_by_date->start_date = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) ); + $sales_by_date->end_date = current_time( 'timestamp' ); + $sales_by_date->chart_groupby = 'day'; + $sales_by_date->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; - $query_from = "FROM {$wpdb->posts} as posts - INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id - INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id - WHERE 1=1 - AND posts.post_type IN ('product', 'product_variation') - AND posts.post_status = 'publish' - AND ( - postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$stock}' AND CAST(postmeta.meta_value AS SIGNED) > '{$nostock}' AND postmeta.meta_value != '' - ) - AND ( - ( postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' ) OR ( posts.post_type = 'product_variation' ) - ) - "; + return $sales_by_date->get_report_data(); + } - $lowinstock_count = absint( $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ) ); + /** + * Show status widget. + */ + public function status_widget() { + include_once( dirname( __FILE__ ) . '/reports/class-wc-admin-report.php' ); - $query_from = "FROM {$wpdb->posts} as posts - INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id - INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id - WHERE 1=1 - AND posts.post_type IN ('product', 'product_variation') - AND posts.post_status = 'publish' - AND ( - postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$nostock}' AND postmeta.meta_value != '' - ) - AND ( - ( postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' ) OR ( posts.post_type = 'product_variation' ) - ) - "; + $reports = new WC_Admin_Report(); - $outofstock_count = absint( $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ) ); - ?> - '; + } + + /** + * Show order data is status widget. + */ + private function status_widget_order_rows() { + if ( ! current_user_can( 'edit_shop_orders' ) ) { + return; + } + $on_hold_count = 0; + $processing_count = 0; + + foreach ( wc_get_order_types( 'order-count' ) as $type ) { + $counts = (array) wp_count_posts( $type ); + $on_hold_count += isset( $counts['wc-on-hold'] ) ? $counts['wc-on-hold'] : 0; + $processing_count += isset( $counts['wc-processing'] ) ? $counts['wc-processing'] : 0; + } + ?> +
  • + + %s order awaiting processing', '%s orders awaiting processing', $processing_count, 'woocommerce' ), + $processing_count + ); + ?> + +
  • +
  • + + %s order on-hold', '%s orders on-hold', $on_hold_count, 'woocommerce' ), + $on_hold_count + ); + ?> + +
  • posts} as posts + INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id + INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id + WHERE 1=1 + AND posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status = 'publish' + AND postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' + AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$stock}' + AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) > '{$nostock}' + " ); + $lowinstock_count = absint( $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ) ); + set_transient( $transient_name, $lowinstock_count, DAY_IN_SECONDS * 30 ); + } + + $transient_name = 'wc_outofstock_count'; + + if ( false === ( $outofstock_count = get_transient( $transient_name ) ) ) { + $query_from = apply_filters( 'woocommerce_report_out_of_stock_query_from', "FROM {$wpdb->posts} as posts + INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id + INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id + WHERE 1=1 + AND posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status = 'publish' + AND postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' + AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$nostock}' + " ); + $outofstock_count = absint( $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ) ); + set_transient( $transient_name, $outofstock_count, DAY_IN_SECONDS * 30 ); + } + ?> +
  • + + %s product low in stock', '%s products low in stock', $lowinstock_count, 'woocommerce' ), + $lowinstock_count + ); + ?> + +
  • +
  • + + %s product out of stock', '%s products out of stock', $outofstock_count, 'woocommerce' ), + $outofstock_count + ); + ?> + +
  • + get_results( "SELECT *, SUBSTRING(comment_content,1,100) AS comment_excerpt - FROM $wpdb->comments - LEFT JOIN $wpdb->posts ON ($wpdb->comments.comment_post_ID = $wpdb->posts.ID) - WHERE comment_approved = '1' - AND comment_type = '' - AND post_password = '' - AND post_type = 'product' - ORDER BY comment_date_gmt DESC - LIMIT 8" ); + + $query_from = apply_filters( 'woocommerce_report_recent_reviews_query_from', "FROM {$wpdb->comments} comments + LEFT JOIN {$wpdb->posts} posts ON (comments.comment_post_ID = posts.ID) + WHERE comments.comment_approved = '1' + AND comments.comment_type = '' + AND posts.post_password = '' + AND posts.post_type = 'product' + AND comments.comment_parent = 0 + ORDER BY comments.comment_date_gmt DESC + LIMIT 5 + " ); + + $comments = $wpdb->get_results( " + SELECT posts.ID, posts.post_title, comments.comment_author, comments.comment_ID, comments.comment_content + {$query_from}; + " ); if ( $comments ) { echo '
      '; @@ -182,13 +271,14 @@ class WC_Admin_Dashboard { echo get_avatar( $comment->comment_author, '32' ); - $rating = get_comment_meta( $comment->comment_ID, 'rating', true ); + $rating = intval( get_comment_meta( $comment->comment_ID, 'rating', true ) ); - echo '
      - ' . $rating . ' ' . __( 'out of 5', 'woocommerce' ) . '
      '; + /* translators: %s: rating */ + echo '
      ' . sprintf( __( '%s out of 5', 'woocommerce' ), $rating ) . '
      '; - echo '

      ' . esc_html__( $comment->post_title ) . ' ' . __( 'reviewed by', 'woocommerce' ) . ' ' . esc_html( $comment->comment_author ) .'

      '; - echo '
      ' . wp_kses_data( $comment->comment_excerpt ) . ' [...]
      '; + /* translators: %s: review author */ + echo '

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

      '; + echo '
      ' . wp_kses_data( $comment->comment_content ) . '
      '; } echo '
    '; @@ -196,7 +286,6 @@ class WC_Admin_Dashboard { echo '

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

    '; } } - } endif; diff --git a/includes/admin/class-wc-admin-duplicate-product.php b/includes/admin/class-wc-admin-duplicate-product.php index 125db9f673d..9f8b7154745 100644 --- a/includes/admin/class-wc-admin-duplicate-product.php +++ b/includes/admin/class-wc-admin-duplicate-product.php @@ -1,70 +1,76 @@ post_type != 'product' ) + if ( 'product' !== $post->post_type ) { return $actions; + } - $actions['duplicate'] = '' . __( 'Duplicate', 'woocommerce' ) . ''; + $actions['duplicate'] = '' . __( 'Duplicate', 'woocommerce' ) . ''; return $actions; } /** - * Show the dupe product link in admin + * Show the dupe product link in admin. */ public function dupe_button() { global $post; - if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) + if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) { return; + } - if ( ! is_object( $post ) ) + if ( ! is_object( $post ) ) { return; + } - if ( $post->post_type != 'product' ) + if ( 'product' !== $post->post_type ) { return; + } if ( isset( $_GET['post'] ) ) { - $notifyUrl = wp_nonce_url( admin_url( "edit.php?post_type=product&action=duplicate_product&post=" . absint( $_GET['post'] ) ), 'woocommerce-duplicate-product_' . $_GET['post'] ); + $notify_url = wp_nonce_url( admin_url( "edit.php?post_type=product&action=duplicate_product&post=" . absint( $_GET['post'] ) ), 'woocommerce-duplicate-product_' . $_GET['post'] ); ?> -
    +
    get_product_to_duplicate( $id ); + $product = wc_get_product( $product_id ); - // Copy the page and insert it - if ( ! empty( $post ) ) { - $new_id = $this->duplicate_product( $post ); - - // If you have written a plugin which uses non-WP database tables to save - // information about a page you can hook this action to dupe that data. - do_action( 'woocommerce_duplicate_product', $new_id, $post ); - - // Redirect to the edit screen for the new draft page - wp_redirect( admin_url( 'post.php?action=edit&post=' . $new_id ) ); - exit; - } else { - wp_die(__( 'Product creation failed, could not find original product:', 'woocommerce' ) . ' ' . $id ); + if ( false === $product ) { + /* translators: %s: product id */ + wp_die( sprintf( __( 'Product creation failed, could not find original product: %s', 'woocommerce' ), $product_id ) ); } + + $duplicate = $this->product_duplicate( $product ); + + // Hook rename to match other woocommerce_product_* hooks, and to move away from depending on a response from the wp_posts table. + do_action( 'woocommerce_product_duplicate', $duplicate, $product ); + wc_do_deprecated_action( 'woocommerce_duplicate_product', array( $duplicate->get_id(), $this->get_product_to_duplicate( $product_id ) ), '3.0', 'Use woocommerce_product_duplicate action instead.' ); + + // Redirect to the edit screen for the new draft page + wp_redirect( admin_url( 'post.php?action=edit&post=' . $duplicate->get_id() ) ); + exit; } /** * Function to create the duplicate of the product. * - * @access public - * @param mixed $post - * @param int $parent (default: 0) - * @param string $post_status (default: '') - * @return int + * @param WC_Product $product + * @return WC_Product */ - public function duplicate_product( $post, $parent = 0, $post_status = '' ) { - global $wpdb; + public function product_duplicate( $product ) { + // Filter to allow us to unset/remove data we don't want to copy to the duplicate. @since 2.6 + $meta_to_exclude = array_filter( apply_filters( 'woocommerce_duplicate_product_exclude_meta', array() ) ); - $new_post_author = wp_get_current_user(); - $new_post_date = current_time('mysql'); - $new_post_date_gmt = get_gmt_from_date($new_post_date); + $duplicate = clone $product; + $duplicate->set_id( 0 ); + $duplicate->set_name( sprintf( __( '%s (Copy)', 'woocommerce' ), $duplicate->get_name() ) ); + $duplicate->set_total_sales( 0 ); + if ( '' !== $product->get_sku( 'edit' ) ) { + $duplicate->set_sku( wc_product_generate_unique_sku( 0, $product->get_sku( 'edit' ) ) ); + } + $duplicate->set_status( 'draft' ); + $duplicate->set_date_created( null ); + $duplicate->set_slug( '' ); + $duplicate->set_rating_counts( 0 ); + $duplicate->set_average_rating( 0 ); + $duplicate->set_review_count( 0 ); - if ( $parent > 0 ) { - $post_parent = $parent; - $post_status = $post_status ? $post_status : 'publish'; - $suffix = ''; - } else { - $post_parent = $post->post_parent; - $post_status = $post_status ? $post_status : 'draft'; - $suffix = ' ' . __( '(Copy)', 'woocommerce' ); + foreach ( $meta_to_exclude as $meta_key ) { + $duplicate->delete_meta_data( $meta_key ); } - $new_post_type = $post->post_type; - $post_content = str_replace("'", "''", $post->post_content); - $post_content_filtered = str_replace("'", "''", $post->post_content_filtered); - $post_excerpt = str_replace("'", "''", $post->post_excerpt); - $post_title = str_replace("'", "''", $post->post_title).$suffix; - $post_name = str_replace("'", "''", $post->post_name); - $comment_status = str_replace("'", "''", $post->comment_status); - $ping_status = str_replace("'", "''", $post->ping_status); + // This action can be used to modify the object further before it is created - it will be passed by reference. @since 3.0 + do_action( 'woocommerce_product_duplicate_before_save', $duplicate, $product ); - // Insert the new template in the post table - $wpdb->query( - "INSERT INTO $wpdb->posts - (post_author, post_date, post_date_gmt, post_content, post_content_filtered, post_title, post_excerpt, post_status, post_type, comment_status, ping_status, post_password, to_ping, pinged, post_modified, post_modified_gmt, post_parent, menu_order, post_mime_type) - VALUES - ('$new_post_author->ID', '$new_post_date', '$new_post_date_gmt', '$post_content', '$post_content_filtered', '$post_title', '$post_excerpt', '$post_status', '$new_post_type', '$comment_status', '$ping_status', '$post->post_password', '$post->to_ping', '$post->pinged', '$new_post_date', '$new_post_date_gmt', '$post_parent', '$post->menu_order', '$post->post_mime_type')"); + // Save parent product. + $duplicate->save(); - $new_post_id = $wpdb->insert_id; + // Duplicate children of a variable product. + if ( ! apply_filters( 'woocommerce_duplicate_product_exclude_children', false, $product ) && $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child_duplicate = clone $child; + $child_duplicate->set_parent_id( $duplicate->get_id() ); + $child_duplicate->set_id( 0 ); - // Copy the taxonomies - $this->duplicate_post_taxonomies( $post->ID, $new_post_id, $post->post_type ); + if ( '' !== $child->get_sku( 'edit' ) ) { + $child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku( 'edit' ) ) ); + } - // Copy the meta information - $this->duplicate_post_meta( $post->ID, $new_post_id ); + foreach ( $meta_to_exclude as $meta_key ) { + $child_duplicate->delete_meta_data( $meta_key ); + } - // Copy the children (variations) - if ( $children_products = get_children( 'post_parent='.$post->ID.'&post_type=product_variation' ) ) { + // This action can be used to modify the object further before it is created - it will be passed by reference. @since 3.0 + do_action( 'woocommerce_product_duplicate_before_save', $child_duplicate, $child ); - if ( $children_products ) - foreach ( $children_products as $child ) - $this->duplicate_product( $this->get_product_to_duplicate( $child->ID ), $new_post_id, $child->post_status ); + $child_duplicate->save(); + } + + // Get new object to reflect new children. + $duplicate = wc_get_product( $duplicate->get_id() ); } - return $new_post_id; + return $duplicate; } /** - * Get a product from the database to duplicate - - * @access public + * Get a product from the database to duplicate. + * + * @deprecated 3.0.0 * @param mixed $id - * @return WP_Post|bool - * @todo Returning false? Need to check for it in... + * @return object|bool * @see duplicate_product */ private function get_product_to_duplicate( $id ) { @@ -175,63 +181,19 @@ class WC_Admin_Duplicate_Product { $id = absint( $id ); - if ( ! $id ) + if ( ! $id ) { return false; + } - $post = $wpdb->get_results( "SELECT * FROM $wpdb->posts WHERE ID=$id" ); + $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) ); - if ( isset( $post->post_type ) && $post->post_type == "revision" ) { + if ( isset( $post->post_type ) && 'revision' === $post->post_type ) { $id = $post->post_parent; - $post = $wpdb->get_results( "SELECT * FROM $wpdb->posts WHERE ID=$id" ); + $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) ); } - return $post[0]; + + return $post; } - - /** - * Copy the taxonomies of a post to another post - * - * @access public - * @param mixed $id - * @param mixed $new_id - * @param mixed $post_type - * @return void - */ - private function duplicate_post_taxonomies( $id, $new_id, $post_type ) { - $taxonomies = get_object_taxonomies($post_type); //array("category", "post_tag"); - foreach ($taxonomies as $taxonomy) { - $post_terms = wp_get_object_terms($id, $taxonomy); - $post_terms_count = sizeof( $post_terms ); - for ($i=0; $i<$post_terms_count; $i++) { - wp_set_object_terms($new_id, $post_terms[$i]->slug, $taxonomy, true); - } - } - } - - /** - * Copy the meta information of a post to another post - * - * @access public - * @param mixed $id - * @param mixed $new_id - * @return void - */ - private function duplicate_post_meta( $id, $new_id ) { - global $wpdb; - $post_meta_infos = $wpdb->get_results("SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=$id"); - - if (count($post_meta_infos)!=0) { - $sql_query_sel = array(); - $sql_query = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) "; - foreach ($post_meta_infos as $meta_info) { - $meta_key = $meta_info->meta_key; - $meta_value = addslashes($meta_info->meta_value); - $sql_query_sel[]= "SELECT $new_id, '$meta_key', '$meta_value'"; - } - $sql_query.= implode(" UNION ALL ", $sql_query_sel); - $wpdb->query($sql_query); - } - } - } endif; diff --git a/includes/admin/class-wc-admin-exporters.php b/includes/admin/class-wc-admin-exporters.php new file mode 100644 index 00000000000..cc9aaf65efb --- /dev/null +++ b/includes/admin/class-wc-admin-exporters.php @@ -0,0 +1,152 @@ +exporters['product_exporter'] = array( + 'menu' => 'edit.php?post_type=product', + 'name' => __( 'Product Export', 'woocommerce' ), + 'capability' => 'edit_products', + 'callback' => array( $this, 'product_exporter' ), + ); + } + + /** + * Add menu items for our custom exporters. + */ + public function add_to_menus() { + foreach ( $this->exporters as $id => $exporter ) { + add_submenu_page( $exporter['menu'], $exporter['name'], $exporter['name'], $exporter['capability'], $id, $exporter['callback'] ); + } + } + + /** + * Hide menu items from view so the pages exist, but the menu items do not. + */ + public function hide_from_menus() { + global $submenu; + + foreach ( $this->exporters as $id => $exporter ) { + if ( isset( $submenu[ $exporter['menu'] ] ) ) { + foreach ( $submenu[ $exporter['menu'] ] as $key => $menu ) { + if ( $id === $menu[2] ) { + unset( $submenu[ $exporter['menu'] ][ $key ] ); + } + } + } + } + } + + /** + * Enqueue scripts. + */ + public function admin_scripts() { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + wp_register_script( 'wc-product-export', WC()->plugin_url() . '/assets/js/admin/wc-product-export' . $suffix . '.js', array( 'jquery' ), WC_VERSION ); + wp_localize_script( 'wc-product-export', 'wc_product_export_params', array( + 'export_nonce' => wp_create_nonce( 'wc-product-export' ), + ) ); + } + + /** + * Export page UI. + */ + public function product_exporter() { + include_once( WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php' ); + include_once( dirname( __FILE__ ) . '/views/html-admin-page-product-export.php' ); + } + + /** + * Serve the generated file. + */ + public function download_export_file() { + if ( isset( $_GET['action'], $_GET['nonce'] ) && wp_verify_nonce( $_GET['nonce'], 'product-csv' ) && 'download_product_csv' === $_GET['action'] ) { + include_once( WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php' ); + $exporter = new WC_Product_CSV_Exporter(); + $exporter->export(); + } + } + + /** + * AJAX callback for doing the actual export to the CSV file. + */ + public function do_ajax_product_export() { + check_ajax_referer( 'wc-product-export', 'security' ); + + if ( ! current_user_can( 'edit_products' ) ) { + wp_die( -1 ); + } + + include_once( WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php' ); + + $step = absint( $_POST['step'] ); + $exporter = new WC_Product_CSV_Exporter(); + + if ( ! empty( $_POST['columns'] ) ) { + $exporter->set_column_names( $_POST['columns'] ); + } + + if ( ! empty( $_POST['selected_columns'] ) ) { + $exporter->set_columns_to_export( $_POST['selected_columns'] ); + } + + if ( ! empty( $_POST['export_meta'] ) ) { + $exporter->enable_meta_export( true ); + } + + if ( ! empty( $_POST['export_types'] ) ) { + $exporter->set_product_types_to_export( $_POST['export_types'] ); + } + + $exporter->set_page( $step ); + $exporter->generate_file(); + + if ( 100 === $exporter->get_percent_complete() ) { + wp_send_json_success( array( + 'step' => 'done', + 'percentage' => 100, + 'url' => add_query_arg( array( 'nonce' => wp_create_nonce( 'product-csv' ), 'action' => 'download_product_csv' ), admin_url( 'edit.php?post_type=product&page=product_exporter' ) ), + ) ); + } else { + wp_send_json_success( array( + 'step' => ++$step, + 'percentage' => $exporter->get_percent_complete(), + 'columns' => $exporter->get_column_names(), + ) ); + } + } +} + +new WC_Admin_Exporters(); diff --git a/includes/admin/class-wc-admin-help.php b/includes/admin/class-wc-admin-help.php index 3144c1aa0be..7fdb0fbea99 100644 --- a/includes/admin/class-wc-admin-help.php +++ b/includes/admin/class-wc-admin-help.php @@ -1,19 +1,21 @@ id, wc_get_screen_ids() ) ) + if ( ! $screen || ! in_array( $screen->id, wc_get_screen_ids() ) ) { return; + } + + $video_map = array( + 'wc-settings' => array( + 'title' => __( 'General Settings', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/mz2l10u5f6.jsonp?', + ), + 'wc-settings-general' => array( + 'title' => __( 'General Settings', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/mz2l10u5f6.jsonp?', + ), + 'wc-settings-products' => array( + 'title' => __( 'Product Settings', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/lolkan4fxf.jsonp?', + ), + 'wc-settings-tax' => array( + 'title' => __( 'Tax Settings', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/qp1v19dwrh.jsonp?', + ), + 'wc-settings-tax-standard' => array( + 'title' => __( 'Tax Rate Example', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/2p903vptwa.jsonp?', + ), + 'wc-settings-tax-reduced-rate' => array( + 'title' => __( 'Tax Rate Example', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/2p903vptwa.jsonp?', + ), + 'wc-settings-tax-zero-rate' => array( + 'title' => __( 'Tax Rate Example', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/2p903vptwa.jsonp?', + ), + 'wc-settings-shipping' => array( + 'title' => __( 'Shipping Zones', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/95yiocro6p.jsonp?', + ), + 'wc-settings-shipping-options' => array( + 'title' => __( 'Shipping Options', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/9c9008dxnr.jsonp?', + ), + 'wc-settings-shipping-classes' => array( + 'title' => __( 'Shipping Classes', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/tpqg17aq99.jsonp?', + ), + 'wc-settings-checkout' => array( + 'title' => __( 'Checkout Settings', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/65yjv96z51.jsonp?', + ), + 'wc-settings-checkout-bacs' => array( + 'title' => __( 'Bank Transfer (BACS) Payment Method', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/dh4piy3sek.jsonp?', + ), + 'wc-settings-checkout-cheque' => array( + 'title' => __( 'Check Payment Method', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/u2m2kcakea.jsonp?', + ), + 'wc-settings-checkout-cod' => array( + 'title' => __( 'Cash on Delivery (COD) Payment Method', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/8hyli8wu5f.jsonp?', + ), + 'wc-settings-checkout-paypal' => array( + 'title' => __( 'PayPal Standard Method', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/rbl7e7l4k2.jsonp?', + ), + 'wc-settings-checkout-paypalbraintree_cards' => array( + 'title' => __( 'PayPal by Braintree Payment Method', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/oyksirgn40.jsonp?', + ), + 'wc-settings-checkout-stripe' => array( + 'title' => __( 'Stripe Payment Method', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/mf975hx5de.jsonp?', + ), + 'wc-settings-account' => array( + 'title' => __( 'Account Settings', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/35mazq7il2.jsonp?', + ), + 'wc-settings-email' => array( + 'title' => __( 'Email Settings', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/svcaftq4xv.jsonp?', + ), + 'wc-settings-api' => array( + 'title' => __( 'API Settings', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/1q0ny74vvq.jsonp?', + ), + 'product' => array( + 'title' => __( 'Creating Products', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/fw0477t6wr.jsonp?', + ), + 'edit-product_cat' => array( + 'title' => __( 'Product Categories', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/f0j5gzqigg.jsonp?', + ), + 'edit-product_tag' => array( + 'title' => __( 'Product Tags', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/f0j5gzqigg.jsonp?', + ), + 'product_attributes' => array( + 'title' => __( 'Product Attributes', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/f0j5gzqigg.jsonp?', + ), + 'wc-status' => array( + 'title' => __( 'System Status', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/xdn733nnhi.jsonp?', + ), + 'wc-reports' => array( + 'title' => __( 'Reports', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/6aasex0w99.jsonp?', + ), + 'edit-shop_coupon' => array( + 'title' => __( 'Coupons', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/gupd4h8sit.jsonp?', + ), + 'shop_coupon' => array( + 'title' => __( 'Coupons', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/gupd4h8sit.jsonp?', + ), + 'edit-shop_order' => array( + 'title' => __( 'Managing Orders', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/n8n0sa8hee.jsonp?', + ), + 'shop_order' => array( + 'title' => __( 'Managing Orders', 'woocommerce' ), + 'url' => '//fast.wistia.net/embed/iframe/n8n0sa8hee.jsonp?', + ), + ); + + $page = empty( $_GET['page'] ) ? '' : sanitize_title( $_GET['page'] ); + $tab = empty( $_GET['tab'] ) ? '' : sanitize_title( $_GET['tab'] ); + $section = empty( $_REQUEST['section'] ) ? '' : sanitize_title( $_REQUEST['section'] ); + $video_key = $page ? implode( '-', array_filter( array( $page, $tab, $section ) ) ) : $screen->id; + + // Fallback for sections + if ( ! isset( $video_map[ $video_key ] ) ) { + $video_key = $page ? implode( '-', array_filter( array( $page, $tab ) ) ) : $screen->id; + } + + // Fallback for tabs + if ( ! isset( $video_map[ $video_key ] ) ) { + $video_key = $page ? $page : $screen->id; + } + + if ( isset( $video_map[ $video_key ] ) ) { + $screen->add_help_tab( array( + 'id' => 'woocommerce_guided_tour_tab', + 'title' => __( 'Guided Tour', 'woocommerce' ), + 'content' => + '

    ' . __( 'Guided Tour', 'woocommerce' ) . ' – ' . esc_html( $video_map[ $video_key ]['title'] ) . '

    ' . + '
    +
    + +
    + ', + ) ); + } $screen->add_help_tab( array( - 'id' => 'woocommerce_docs_tab', - 'title' => __( 'Documentation', 'woocommerce' ), - 'content' => + 'id' => 'woocommerce_support_tab', + 'title' => __( 'Help & Support', 'woocommerce' ), + 'content' => + '

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

    ' . + '

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

    ' . + '

    ' . sprintf( + __( 'For further assistance with WooCommerce core you can use the community forum. If you need help with premium extensions sold by WooCommerce, please use our helpdesk.', 'woocommerce' ), + 'https://wordpress.org/support/plugin/woocommerce', + 'https://woocommerce.com/my-account/tickets/?utm_source=helptab&utm_medium=product&utm_content=tickets&utm_campaign=woocommerceplugin' + ) . '

    ' . + '

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

    ' . + '

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

    ', + ) ); - '

    ' . __( 'Thank you for using WooCommerce :) Should you need help using or extending WooCommerce please read the documentation.', 'woocommerce' ) . '

    ' . - - '

    ' . __( 'WooCommerce Documentation', 'woocommerce' ) . ' ' . __( 'Developer API Docs', 'woocommerce' ) . '

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

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

    ' . + '

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

    ' . + '

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

    ', ) ); $screen->add_help_tab( array( - 'id' => 'woocommerce_support_tab', - 'title' => __( 'Support', 'woocommerce' ), - 'content' => - - '

    ' . sprintf(__( 'After reading the documentation, for further assistance you can use the community forum, or if you have access as a WooThemes customer, our support desk.', 'woocommerce' ), 'http://docs.woothemes.com/documentation/plugins/woocommerce/', 'http://wordpress.org/support/plugin/woocommerce', 'http://support.woothemes.com' ) . '

    ' . - - '

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

    ' . - - '

    ' . __( 'System Status', 'woocommerce' ) . ' ' . __( 'Community Support', 'woocommerce' ) . ' ' . __( 'Customer Support', 'woocommerce' ) . '

    ' - + 'id' => 'woocommerce_education_tab', + 'title' => __( 'Education', 'woocommerce' ), + 'content' => + '

    ' . __( 'Education', 'woocommerce' ) . '

    ' . + '

    ' . __( 'If you would like to learn about using WooCommerce from an expert, consider following a WooCommerce course offered by one of our educational partners.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'View education partners', 'woocommerce' ) . '

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

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

    ' . - - '

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

    ' + 'id' => 'woocommerce_onboard_tab', + 'title' => __( 'Setup wizard', 'woocommerce' ), + 'content' => + '

    ' . __( 'Setup wizard', 'woocommerce' ) . '

    ' . + '

    ' . __( 'If you need to access the setup wizard again, please click on the button below.', 'woocommerce' ) . '

    ' . + '

    ' . __( 'Setup wizard', 'woocommerce' ) . '

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

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

    ' . - '

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

    ' . - '

    ' . __( 'Project on WordPress.org', 'woocommerce' ) . '

    ' . - '

    ' . __( 'Project on Github', 'woocommerce' ) . '

    ' . - '

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

    ' . - '

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

    ' + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' ); } - } endif; -return new WC_Admin_Help(); \ No newline at end of file +return new WC_Admin_Help(); diff --git a/includes/admin/class-wc-admin-importers.php b/includes/admin/class-wc-admin-importers.php index c802a563014..230c7eb9a42 100644 --- a/includes/admin/class-wc-admin-importers.php +++ b/includes/admin/class-wc-admin-importers.php @@ -1,39 +1,114 @@ importers['product_importer'] = array( + 'menu' => 'edit.php?post_type=product', + 'name' => __( 'Product Import', 'woocommerce' ), + 'capability' => 'edit_products', + 'callback' => array( $this, 'product_importer' ), + ); } /** - * Add menu items + * Add menu items for our custom importers. + */ + public function add_to_menus() { + foreach ( $this->importers as $id => $importer ) { + add_submenu_page( $importer['menu'], $importer['name'], $importer['name'], $importer['capability'], $id, $importer['callback'] ); + } + } + + /** + * Hide menu items from view so the pages exist, but the menu items do not. + */ + public function hide_from_menus() { + global $submenu; + + foreach ( $this->importers as $id => $importer ) { + if ( isset( $submenu[ $importer['menu'] ] ) ) { + foreach ( $submenu[ $importer['menu'] ] as $key => $menu ) { + if ( $id === $menu[2] ) { + unset( $submenu[ $importer['menu'] ][ $key ] ); + } + } + } + } + } + + /** + * Register importer scripts. + */ + public function admin_scripts() { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + wp_register_script( 'wc-product-import', WC()->plugin_url() . '/assets/js/admin/wc-product-import' . $suffix . '.js', array( 'jquery' ), WC_VERSION ); + } + + /** + * The product importer. + * + * This has a custom screen - the Tools > Import item is a placeholder. + * If we're on that screen, redirect to the custom one. + */ + public function product_importer() { + if ( defined( 'WP_LOAD_IMPORTERS' ) ) { + wp_safe_redirect( admin_url( 'edit.php?post_type=product&page=product_importer' ) ); + exit; + } + + include_once( WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php' ); + include_once( WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php' ); + + $importer = new WC_Product_CSV_Importer_Controller(); + $importer->dispatch(); + } + + /** + * Register WordPress based importers. */ public function register_importers() { - register_importer( 'woocommerce_tax_rate_csv', __( 'WooCommerce Tax Rates (CSV)', 'woocommerce' ), __( 'Import tax rates to your store via a csv file.', 'woocommerce'), array( $this, 'tax_rates_importer' ) ); + if ( defined( 'WP_LOAD_IMPORTERS' ) ) { + add_action( 'import_start', array( $this, 'post_importer_compatibility' ) ); + register_importer( 'woocommerce_product_csv', __( 'WooCommerce products (CSV)', 'woocommerce' ), __( 'Import products to your store via a csv file.', 'woocommerce' ), array( $this, 'product_importer' ) ); + register_importer( 'woocommerce_tax_rate_csv', __( 'WooCommerce tax rates (CSV)', 'woocommerce' ), __( 'Import tax rates to your store via a csv file.', 'woocommerce' ), array( $this, 'tax_rates_importer' ) ); + } } /** - * Add menu item + * The tax rate importer which extends WP_Importer. */ public function tax_rates_importer() { // Load Importer API @@ -41,12 +116,14 @@ class WC_Admin_Importers { if ( ! class_exists( 'WP_Importer' ) ) { $class_wp_importer = ABSPATH . 'wp-admin/includes/class-wp-importer.php'; - if ( file_exists( $class_wp_importer ) ) + + if ( file_exists( $class_wp_importer ) ) { require $class_wp_importer; + } } // includes - require 'importers/class-wc-tax-rate-importer.php'; + require( dirname( __FILE__ ) . '/importers/class-wc-tax-rate-importer.php' ); // Dispatch $importer = new WC_Tax_Rate_Importer(); @@ -54,62 +131,55 @@ class WC_Admin_Importers { } /** - * When running the WP importer, ensure attributes exist. + * When running the WP XML importer, ensure attributes exist. * * WordPress import should work - however, it fails to import custom product attribute taxonomies. * This code grabs the file before it is imported and ensures the taxonomies are created. - * - * @access public - * @return void */ public function post_importer_compatibility() { global $wpdb; - if ( empty( $_POST['import_id'] ) || ! class_exists( 'WXR_Parser' ) ) + if ( empty( $_POST['import_id'] ) || ! class_exists( 'WXR_Parser' ) ) { return; + } - $id = (int) $_POST['import_id']; + $id = absint( $_POST['import_id'] ); $file = get_attached_file( $id ); $parser = new WXR_Parser(); $import_data = $parser->parse( $file ); - if ( isset( $import_data['posts'] ) ) { - $posts = $import_data['posts']; + if ( isset( $import_data['posts'] ) && ! empty( $import_data['posts'] ) ) { + foreach ( $import_data['posts'] as $post ) { + if ( 'product' === $post['post_type'] && ! empty( $post['terms'] ) ) { + foreach ( $post['terms'] as $term ) { + if ( strstr( $term['domain'], 'pa_' ) ) { + if ( ! taxonomy_exists( $term['domain'] ) ) { + $attribute_name = wc_sanitize_taxonomy_name( str_replace( 'pa_', '', $term['domain'] ) ); - if ( $posts && sizeof( $posts ) > 0 ) foreach ( $posts as $post ) { - - if ( $post['post_type'] == 'product' ) { - - if ( $post['terms'] && sizeof( $post['terms'] ) > 0 ) { - - foreach ( $post['terms'] as $term ) { - - $domain = $term['domain']; - - if ( strstr( $domain, 'pa_' ) ) { - - // Make sure it exists! - if ( ! taxonomy_exists( $domain ) ) { - - $nicename = strtolower( sanitize_title( str_replace( 'pa_', '', $domain ) ) ); - - $exists_in_db = $wpdb->get_var( $wpdb->prepare( "SELECT attribute_id FROM " . $wpdb->prefix . "woocommerce_attribute_taxonomies WHERE attribute_name = %s;", $nicename ) ); - - // Create the taxonomy - if ( ! $exists_in_db ) - $wpdb->insert( $wpdb->prefix . "woocommerce_attribute_taxonomies", array( 'attribute_name' => $nicename, 'attribute_type' => 'select', 'attribute_orderby' => 'menu_order' ), array( '%s', '%s', '%s' ) ); - - // Register the taxonomy now so that the import works! - register_taxonomy( $domain, - apply_filters( 'woocommerce_taxonomy_objects_' . $domain, array('product') ), - apply_filters( 'woocommerce_taxonomy_args_' . $domain, array( - 'hierarchical' => true, - 'show_ui' => false, - 'query_var' => true, - 'rewrite' => false, - ) ) - ); + // Create the taxonomy + if ( ! in_array( $attribute_name, wc_get_attribute_taxonomies() ) ) { + $attribute = array( + 'attribute_label' => $attribute_name, + 'attribute_name' => $attribute_name, + 'attribute_type' => 'select', + 'attribute_orderby' => 'menu_order', + 'attribute_public' => 0, + ); + $wpdb->insert( $wpdb->prefix . 'woocommerce_attribute_taxonomies', $attribute ); + delete_transient( 'wc_attribute_taxonomies' ); } + + // Register the taxonomy now so that the import works! + register_taxonomy( + $term['domain'], + apply_filters( 'woocommerce_taxonomy_objects_' . $term['domain'], array( 'product' ) ), + apply_filters( 'woocommerce_taxonomy_args_' . $term['domain'], array( + 'hierarchical' => true, + 'show_ui' => false, + 'query_var' => true, + 'rewrite' => false, + ) ) + ); } } } @@ -117,8 +187,73 @@ class WC_Admin_Importers { } } } + + /** + * Ajax callback for importing one batch of products from a CSV. + */ + public function do_ajax_product_import() { + global $wpdb; + + check_ajax_referer( 'wc-product-import', 'security' ); + + if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['file'] ) ) { + wp_die( -1 ); + } + + include_once( WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php' ); + include_once( WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php' ); + + $file = wc_clean( $_POST['file'] ); + $params = array( + 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( $_POST['delimiter'] ) : ',', + 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, + 'mapping' => isset( $_POST['mapping'] ) ? (array) $_POST['mapping'] : array(), + 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, + 'lines' => apply_filters( 'woocommerce_product_import_batch_size', 30 ), + 'parse' => true, + ); + + // Log failures. + if ( 0 !== $params['start_pos'] ) { + $error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) ); + } else { + $error_log = array(); + } + + $importer = WC_Product_CSV_Importer_Controller::get_importer( $file, $params ); + $results = $importer->import(); + $percent_complete = $importer->get_percent_complete(); + $error_log = array_merge( $error_log, $results['failed'], $results['skipped'] ); + + update_user_option( get_current_user_id(), 'product_import_error_log', $error_log ); + + if ( 100 === $percent_complete ) { + // Clear temp meta. + $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) ); + $wpdb->delete( $wpdb->posts, array( 'post_status' => 'importing', 'post_type' => 'product' ) ); + $wpdb->delete( $wpdb->posts, array( 'post_status' => 'importing', 'post_type' => 'product_variation' ) ); + + // Send success. + wp_send_json_success( array( + 'position' => 'done', + 'percentage' => 100, + 'url' => add_query_arg( array( 'nonce' => wp_create_nonce( 'product-csv' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ), + 'imported' => count( $results['imported'] ), + 'failed' => count( $results['failed'] ), + 'updated' => count( $results['updated'] ), + 'skipped' => count( $results['skipped'] ), + ) ); + } else { + wp_send_json_success( array( + 'position' => $importer->get_file_position(), + 'percentage' => $percent_complete, + 'imported' => count( $results['imported'] ), + 'failed' => count( $results['failed'] ), + 'updated' => count( $results['updated'] ), + 'skipped' => count( $results['skipped'] ), + ) ); + } + } } -endif; - -return new WC_Admin_Importers(); \ No newline at end of file +new WC_Admin_Importers(); diff --git a/includes/admin/class-wc-admin-log-table-list.php b/includes/admin/class-wc-admin-log-table-list.php new file mode 100644 index 00000000000..6b350a5bc76 --- /dev/null +++ b/includes/admin/class-wc-admin-log-table-list.php @@ -0,0 +1,355 @@ + 'log', + 'plural' => 'logs', + 'ajax' => false, + ) ); + } + + /** + * Display level dropdown + * + * @global wpdb $wpdb + */ + public function level_dropdown() { + + $levels = array( + array( 'value' => WC_Log_Levels::EMERGENCY, 'label' => __( 'Emergency', 'woocommerce' ) ), + array( 'value' => WC_Log_Levels::ALERT, 'label' => __( 'Alert', 'woocommerce' ) ), + array( 'value' => WC_Log_Levels::CRITICAL, 'label' => __( 'Critical', 'woocommerce' ) ), + array( 'value' => WC_Log_Levels::ERROR, 'label' => __( 'Error', 'woocommerce' ) ), + array( 'value' => WC_Log_Levels::WARNING, 'label' => __( 'Warning', 'woocommerce' ) ), + array( 'value' => WC_Log_Levels::NOTICE, 'label' => __( 'Notice', 'woocommerce' ) ), + array( 'value' => WC_Log_Levels::INFO, 'label' => __( 'Info', 'woocommerce' ) ), + array( 'value' => WC_Log_Levels::DEBUG, 'label' => __( 'Debug', 'woocommerce' ) ), + ); + + $selected_level = isset( $_REQUEST['level'] ) ? $_REQUEST['level'] : ''; + ?> + + + '', + 'timestamp' => __( 'Timestamp', 'woocommerce' ), + 'level' => __( 'Level', 'woocommerce' ), + 'message' => __( 'Message', 'woocommerce' ), + 'source' => __( 'Source', 'woocommerce' ), + ); + } + + /** + * Column cb. + * + * @param array $log + * @return string + */ + public function column_cb( $log ) { + return sprintf( '', esc_attr( $log['log_id'] ) ); + } + + /** + * Timestamp column. + * + * @param array $log + * @return string + */ + public function column_timestamp( $log ) { + return esc_html( mysql2date( + get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), + $log['timestamp'] + ) ); + } + + /** + * Level column. + * + * @param array $log + * @return string + */ + public function column_level( $log ) { + $level_key = WC_Log_Levels::get_severity_level( $log['level'] ); + $levels = array( + 'emergency' => __( 'Emergency', 'woocommerce' ), + 'alert' => __( 'Alert', 'woocommerce' ), + 'critical' => __( 'Critical', 'woocommerce' ), + 'error' => __( 'Error', 'woocommerce' ), + 'warning' => __( 'Warning', 'woocommerce' ), + 'notice' => __( 'Notice', 'woocommerce' ), + 'info' => __( 'Info', 'woocommerce' ), + 'debug' => __( 'Debug', 'woocommerce' ), + ); + + if ( isset( $levels[ $level_key ] ) ) { + $level = $levels[ $level_key ]; + $level_class = sanitize_html_class( 'log-level--' . $level_key ); + return '' . esc_html( $level ) . ''; + } else { + return ''; + } + } + + /** + * Message column. + * + * @param array $log + * @return string + */ + public function column_message( $log ) { + return esc_html( $log['message'] ); + } + + /** + * Source column. + * + * @param array $log + * @return string + */ + public function column_source( $log ) { + return esc_html( $log['source'] ); + } + + /** + * Get bulk actions. + * + * @return array + */ + protected function get_bulk_actions() { + return array( + 'delete' => __( 'Delete', 'woocommerce' ), + ); + } + + /** + * Extra controls to be displayed between bulk actions and pagination. + * + * @param string $which + */ + protected function extra_tablenav( $which ) { + if ( 'top' === $which ) { + echo '
    '; + $this->level_dropdown(); + $this->source_dropdown(); + submit_button( __( 'Filter', 'woocommerce' ), '', 'filter-action', false ); + echo '
    '; + } + } + + /** + * Get a list of sortable columns. + * + * @return array + */ + protected function get_sortable_columns() { + return array( + 'timestamp' => array( 'timestamp', true ), + 'level' => array( 'level', true ), + 'source' => array( 'source', true ), + ); + } + + /** + * Display source dropdown + * + * @global wpdb $wpdb + */ + protected function source_dropdown() { + global $wpdb; + + $sources = $wpdb->get_col( " + SELECT DISTINCT source + FROM {$wpdb->prefix}woocommerce_log + WHERE source != '' + ORDER BY source ASC + " ); + + if ( ! empty( $sources ) ) { + $selected_source = isset( $_REQUEST['source'] ) ? $_REQUEST['source'] : ''; + ?> + + + prepare_column_headers(); + + $per_page = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 ); + + $where = $this->get_items_query_where(); + $order = $this->get_items_query_order(); + $limit = $this->get_items_query_limit(); + $offset = $this->get_items_query_offset(); + + $query_items = " + SELECT log_id, timestamp, level, message, source + FROM {$wpdb->prefix}woocommerce_log + {$where} {$order} {$limit} {$offset} + "; + + $this->items = $wpdb->get_results( $query_items, ARRAY_A ); + + $query_count = "SELECT COUNT(log_id) FROM {$wpdb->prefix}woocommerce_log {$where}"; + $total_items = $wpdb->get_var( $query_count ); + + $this->set_pagination_args( array( + 'total_items' => $total_items, + 'per_page' => $per_page, + 'total_pages' => ceil( $total_items / $per_page ), + ) ); + } + + /** + * Get prepared LIMIT clause for items query + * + * @global wpdb $wpdb + * + * @return string Prepared LIMIT clause for items query. + */ + protected function get_items_query_limit() { + global $wpdb; + + $per_page = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 ); + return $wpdb->prepare( 'LIMIT %d', $per_page ); + } + + /** + * Get prepared OFFSET clause for items query + * + * @global wpdb $wpdb + * + * @return string Prepared OFFSET clause for items query. + */ + protected function get_items_query_offset() { + global $wpdb; + + $per_page = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 ); + $current_page = $this->get_pagenum(); + if ( 1 < $current_page ) { + $offset = $per_page * ( $current_page - 1 ); + } else { + $offset = 0; + } + + return $wpdb->prepare( 'OFFSET %d', $offset ); + } + + /** + * Get prepared ORDER BY clause for items query + * + * @return string Prepared ORDER BY clause for items query. + */ + protected function get_items_query_order() { + $valid_orders = array( 'level', 'source', 'timestamp' ); + if ( ! empty( $_REQUEST['orderby'] ) && in_array( $_REQUEST['orderby'], $valid_orders ) ) { + $by = wc_clean( $_REQUEST['orderby'] ); + } else { + $by = 'timestamp'; + } + $by = esc_sql( $by ); + + if ( ! empty( $_REQUEST['order'] ) && 'asc' === strtolower( $_REQUEST['order'] ) ) { + $order = 'ASC'; + } else { + $order = 'DESC'; + } + + return "ORDER BY {$by} {$order}, log_id {$order}"; + } + + /** + * Get prepared WHERE clause for items query + * + * @global wpdb $wpdb + * + * @return string Prepared WHERE clause for items query. + */ + protected function get_items_query_where() { + global $wpdb; + + $where_conditions = array(); + $where_values = array(); + if ( ! empty( $_REQUEST['level'] ) && WC_Log_Levels::is_valid_level( $_REQUEST['level'] ) ) { + $where_conditions[] = 'level >= %d'; + $where_values[] = WC_Log_Levels::get_level_severity( $_REQUEST['level'] ); + } + if ( ! empty( $_REQUEST['source'] ) ) { + $where_conditions[] = 'source = %s'; + $where_values[] = wc_clean( $_REQUEST['source'] ); + } + + if ( ! empty( $where_conditions ) ) { + return $wpdb->prepare( 'WHERE 1 = 1 AND ' . implode( ' AND ', $where_conditions ), $where_values ); + } else { + return ''; + } + } + + /** + * Set _column_headers property for table list + */ + protected function prepare_column_headers() { + $this->_column_headers = array( + $this->get_columns(), + array(), + $this->get_sortable_columns(), + ); + } +} diff --git a/includes/admin/class-wc-admin-menus.php b/includes/admin/class-wc-admin-menus.php index 5011aa2b567..09aee416b64 100644 --- a/includes/admin/class-wc-admin-menus.php +++ b/includes/admin/class-wc-admin-menus.php @@ -2,18 +2,20 @@ /** * Setup menus in WP admin. * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin - * @version 2.1.0 + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin + * @version 2.5.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; +} -if ( ! class_exists( 'WC_Admin_Menus' ) ) : +if ( ! class_exists( 'WC_Admin_Menus', false ) ) : /** - * WC_Admin_Menus Class + * WC_Admin_Menus Class. */ class WC_Admin_Menus { @@ -32,36 +34,50 @@ class WC_Admin_Menus { } add_action( 'admin_head', array( $this, 'menu_highlight' ) ); + add_action( 'admin_head', array( $this, 'menu_order_count' ) ); add_filter( 'menu_order', array( $this, 'menu_order' ) ); add_filter( 'custom_menu_order', array( $this, 'custom_menu_order' ) ); + + // Add endpoints custom URLs in Appearance > Menus > Pages. + add_action( 'admin_head-nav-menus.php', array( $this, 'add_nav_menu_meta_boxes' ) ); + + // Admin bar menus + if ( apply_filters( 'woocommerce_show_admin_bar_visit_store', true ) ) { + add_action( 'admin_bar_menu', array( $this, 'admin_bar_menus' ), 31 ); + } } /** - * Add menu items + * Add menu items. */ public function admin_menu() { global $menu; - if ( current_user_can( 'manage_woocommerce' ) ) - $menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); + if ( current_user_can( 'manage_woocommerce' ) ) { + $menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); + } - $main_page = add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'manage_woocommerce', 'woocommerce' , array( $this, 'settings_page' ), null, '55.5' ); + add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'manage_woocommerce', 'woocommerce', null, null, '55.5' ); - add_submenu_page( 'edit.php?post_type=product', __( 'Attributes', 'woocommerce' ), __( 'Attributes', 'woocommerce' ), 'manage_product_terms', 'product_attributes', array( $this, 'attributes_page' ) ); + add_submenu_page( 'edit.php?post_type=product', __( 'Attributes', 'woocommerce' ), __( 'Attributes', 'woocommerce' ), 'manage_product_terms', 'product_attributes', array( $this, 'attributes_page' ) ); } /** - * Add menu item + * Add menu item. */ public function reports_menu() { - add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ) , 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) ); + if ( current_user_can( 'manage_woocommerce' ) ) { + add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ) , 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) ); + } else { + add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ) , 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), null, '55.6' ); + } } /** - * Add menu item + * Add menu item. */ public function settings_menu() { - $settings_page = add_submenu_page( 'woocommerce', __( 'WooCommerce Settings', 'woocommerce' ), __( 'Settings', 'woocommerce' ) , 'manage_woocommerce', 'wc-settings', array( $this, 'settings_page' ) ); + $settings_page = add_submenu_page( 'woocommerce', __( 'WooCommerce settings', 'woocommerce' ), __( 'Settings', 'woocommerce' ) , 'manage_woocommerce', 'wc-settings', array( $this, 'settings_page' ) ); add_action( 'load-' . $settings_page, array( $this, 'settings_page_init' ) ); } @@ -75,66 +91,57 @@ class WC_Admin_Menus { } /** - * Add menu item + * Add menu item. */ public function status_menu() { - add_submenu_page( 'woocommerce', __( 'WooCommerce Status', 'woocommerce' ), __( 'System Status', 'woocommerce' ) , 'manage_woocommerce', 'wc-status', array( $this, 'status_page' ) ); - register_setting( 'woocommerce_status_settings_fields', 'woocommerce_status_options' ); + add_submenu_page( 'woocommerce', __( 'WooCommerce status', 'woocommerce' ), __( 'Status', 'woocommerce' ) , 'manage_woocommerce', 'wc-status', array( $this, 'status_page' ) ); } /** - * Addons menu item + * Addons menu item. */ public function addons_menu() { - add_submenu_page( 'woocommerce', __( 'WooCommerce Add-ons/Extensions', 'woocommerce' ), __( 'Add-ons', 'woocommerce' ) , 'manage_woocommerce', 'wc-addons', array( $this, 'addons_page' ) ); + add_submenu_page( 'woocommerce', __( 'WooCommerce extensions', 'woocommerce' ), __( 'Extensions', 'woocommerce' ) , 'manage_woocommerce', 'wc-addons', array( $this, 'addons_page' ) ); } /** * Highlights the correct top level admin menu item for post type add screens. - * - * @access public - * @return void */ public function menu_highlight() { - global $menu, $submenu, $parent_file, $submenu_file, $self, $post_type, $taxonomy; + global $parent_file, $submenu_file, $post_type; - $to_highlight_types = array( 'shop_order', 'shop_coupon' ); - - if ( isset( $post_type ) ) { - if ( in_array( $post_type, $to_highlight_types ) ) { - $submenu_file = 'edit.php?post_type=' . esc_attr( $post_type ); - $parent_file = 'woocommerce'; - } - - if ( 'product' == $post_type ) { + switch ( $post_type ) { + case 'shop_order' : + case 'shop_coupon' : + $parent_file = 'woocommerce'; + break; + case 'product' : $screen = get_current_screen(); - - if ( $screen->base == 'edit-tags' && taxonomy_is_product_attribute( $taxonomy ) ) { + if ( $screen && taxonomy_is_product_attribute( $screen->taxonomy ) ) { $submenu_file = 'product_attributes'; - $parent_file = 'edit.php?post_type=' . esc_attr( $post_type ); + $parent_file = 'edit.php?post_type=product'; } - } + break; } + } - if ( isset( $submenu['woocommerce'] ) && isset( $submenu['woocommerce'][1] ) ) { - $submenu['woocommerce'][0] = $submenu['woocommerce'][1]; - unset( $submenu['woocommerce'][1] ); - } + /** + * Adds the order processing count to the menu. + */ + public function menu_order_count() { + global $submenu; - // Sort out Orders menu when on the top level - if ( ! current_user_can( 'manage_woocommerce' ) ) { - foreach ( $menu as $key => $menu_item ) { - if ( strpos( $menu_item[0], _x('Orders', 'Admin menu name', 'woocommerce') ) === 0 ) { + if ( isset( $submenu['woocommerce'] ) ) { + // Remove 'WooCommerce' sub menu item + unset( $submenu['woocommerce'][0] ); - $menu_name = _x('Orders', 'Admin menu name', 'woocommerce'); - $menu_name_count = ''; - if ( $order_count = wc_processing_order_count() ) { - $menu_name_count = " " . number_format_i18n( $order_count ) . "" ; + // Add count if user has access + if ( apply_filters( 'woocommerce_include_processing_order_count_in_menu', true ) && current_user_can( 'manage_woocommerce' ) && ( $order_count = wc_processing_order_count() ) ) { + foreach ( $submenu['woocommerce'] as $key => $menu_item ) { + if ( 0 === strpos( $menu_item[0], _x( 'Orders', 'Admin menu name', 'woocommerce' ) ) ) { + $submenu['woocommerce'][ $key ][0] .= ' ' . number_format_i18n( $order_count ) . ''; + break; } - - $menu[$key][0] = $menu_name . $menu_name_count; - $submenu['edit.php?post_type=shop_order'][5][0] = $menu_name; - break; } } } @@ -157,71 +164,160 @@ class WC_Admin_Menus { $woocommerce_product = array_search( 'edit.php?post_type=product', $menu_order ); // Loop through menu order and do some rearranging - foreach ( $menu_order as $index => $item ) : + foreach ( $menu_order as $index => $item ) { - if ( ( ( 'woocommerce' ) == $item ) ) : + if ( ( ( 'woocommerce' ) == $item ) ) { $woocommerce_menu_order[] = 'separator-woocommerce'; $woocommerce_menu_order[] = $item; $woocommerce_menu_order[] = 'edit.php?post_type=product'; - unset( $menu_order[$woocommerce_separator] ); - unset( $menu_order[$woocommerce_product] ); - elseif ( !in_array( $item, array( 'separator-woocommerce' ) ) ) : + unset( $menu_order[ $woocommerce_separator ] ); + unset( $menu_order[ $woocommerce_product ] ); + } elseif ( ! in_array( $item, array( 'separator-woocommerce' ) ) ) { $woocommerce_menu_order[] = $item; - endif; - - endforeach; + } + } // Return order return $woocommerce_menu_order; } /** - * custom_menu_order + * Custom menu order. + * * @return bool */ public function custom_menu_order() { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return false; - } - return true; + return current_user_can( 'manage_woocommerce' ); } /** - * Init the reports page + * Init the reports page. */ public function reports_page() { WC_Admin_Reports::output(); } /** - * Init the settings page + * Init the settings page. */ public function settings_page() { WC_Admin_Settings::output(); } /** - * Init the attributes page + * Init the attributes page. */ public function attributes_page() { WC_Admin_Attributes::output(); } /** - * Init the status page + * Init the status page. */ public function status_page() { WC_Admin_Status::output(); } /** - * Init the addons page + * Init the addons page. */ public function addons_page() { WC_Admin_Addons::output(); } + + /** + * Add custom nav meta box. + * + * Adapted from http://www.johnmorrisonline.com/how-to-add-a-fully-functional-custom-meta-box-to-wordpress-navigation-menus/. + */ + public function add_nav_menu_meta_boxes() { + add_meta_box( 'woocommerce_endpoints_nav_link', __( 'WooCommerce endpoints', 'woocommerce' ), array( $this, 'nav_menu_links' ), 'nav-menus', 'side', 'low' ); + } + + /** + * Output menu links. + */ + public function nav_menu_links() { + // Get items from account menu. + $endpoints = wc_get_account_menu_items(); + + // Remove dashboard item. + if ( isset( $endpoints['dashboard'] ) ) { + unset( $endpoints['dashboard'] ); + } + + // Include missing lost password. + $endpoints['lost-password'] = __( 'Lost password', 'woocommerce' ); + + $endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints ); + + ?> +
    +
    +
      + $value ) : + ?> +
    • + + + + + +
    • + +
    +
    +

    + + + + + + + +

    +
    + add_node( array( + 'parent' => 'site-name', + 'id' => 'view-store', + 'title' => __( 'Visit Store', 'woocommerce' ), + 'href' => wc_get_page_permalink( 'shop' ), + ) ); + } } endif; -return new WC_Admin_Menus(); \ No newline at end of file +return new WC_Admin_Menus(); diff --git a/includes/admin/class-wc-admin-meta-boxes.php b/includes/admin/class-wc-admin-meta-boxes.php index 175a122f1c5..68641046f76 100644 --- a/includes/admin/class-wc-admin-meta-boxes.php +++ b/includes/admin/class-wc-admin-meta-boxes.php @@ -2,66 +2,78 @@ /** * WooCommerce Meta Boxes * - * Sets up the write panels used by products and orders (custom post types) + * Sets up the write panels used by products and orders (custom post types). * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin/Meta Boxes * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} /** - * WC_Admin_Meta_Boxes + * WC_Admin_Meta_Boxes. */ class WC_Admin_Meta_Boxes { - private static $meta_box_errors = array(); + /** + * Is meta boxes saved once? + * + * @var boolean + */ + private static $saved_meta_boxes = false; /** - * Constructor + * Meta box error messages. + * + * @var array + */ + public static $meta_box_errors = array(); + + /** + * Constructor. */ public function __construct() { add_action( 'add_meta_boxes', array( $this, 'remove_meta_boxes' ), 10 ); add_action( 'add_meta_boxes', array( $this, 'rename_meta_boxes' ), 20 ); add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 30 ); - add_action( 'add_meta_boxes', array( 'WC_Gateway_Mijireh', 'add_page_slurp_meta' ) ); add_action( 'save_post', array( $this, 'save_meta_boxes' ), 1, 2 ); /** - * Save Order Meta Boxes + * Save Order Meta Boxes. * * In order: - * Save the order items - * Save the order totals - * Save the order downloads - * Save order data - also updates status and sends out admin emails if needed. Last to show latest data. - * Save actions - sends out other emails. Last to show latest data. + * Save the order items. + * Save the order totals. + * Save the order downloads. + * Save order data - also updates status and sends out admin emails if needed. Last to show latest data. + * Save actions - sends out other emails. Last to show latest data. */ add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Items::save', 10, 2 ); - add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Totals::save', 20, 2 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30, 2 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Data::save', 40, 2 ); add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Actions::save', 50, 2 ); - // Save Product Meta Boxes + // Save Product Meta Boxes. add_action( 'woocommerce_process_product_meta', 'WC_Meta_Box_Product_Data::save', 10, 2 ); add_action( 'woocommerce_process_product_meta', 'WC_Meta_Box_Product_Images::save', 20, 2 ); - // Save Coupon Meta Boxes + // Save Coupon Meta Boxes. add_action( 'woocommerce_process_shop_coupon_meta', 'WC_Meta_Box_Coupon_Data::save', 10, 2 ); - // Save Rating Meta Boxes - add_action( 'comment_edit_redirect', 'WC_Meta_Box_Order_Reviews::save', 1, 2 ); + // Save Rating Meta Boxes. + add_action( 'comment_edit_redirect', 'WC_Meta_Box_Product_Reviews::save', 1, 2 ); - // Error handling (for showing errors from meta boxes on next page load) + // Error handling (for showing errors from meta boxes on next page load). add_action( 'admin_notices', array( $this, 'output_errors' ) ); add_action( 'shutdown', array( $this, 'save_errors' ) ); } /** - * Add an error message + * Add an error message. * @param string $text */ public static function add_error( $text ) { @@ -69,7 +81,7 @@ class WC_Admin_Meta_Boxes { } /** - * Save errors to an option + * Save errors to an option. */ public function save_errors() { update_option( 'woocommerce_meta_box_errors', self::$meta_box_errors ); @@ -79,14 +91,16 @@ class WC_Admin_Meta_Boxes { * Show any stored error messages. */ public function output_errors() { - $errors = maybe_unserialize( get_option( 'woocommerce_meta_box_errors' ) ); + $errors = array_filter( (array) get_option( 'woocommerce_meta_box_errors' ) ); if ( ! empty( $errors ) ) { - echo '
    '; + echo '
    '; + foreach ( $errors as $error ) { - echo '

    ' . esc_html( $error ) . '

    '; + echo '

    ' . wp_kses_post( $error ) . '

    '; } + echo '
    '; // Clear @@ -95,74 +109,80 @@ class WC_Admin_Meta_Boxes { } /** - * Add WC Meta boxes + * Add WC Meta boxes. */ public function add_meta_boxes() { - // Products - add_meta_box( 'postexcerpt', __( 'Product Short Description', 'woocommerce' ), 'WC_Meta_Box_Product_Short_Description::output', 'product', 'normal' ); - add_meta_box( 'woocommerce-product-data', __( 'Product Data', 'woocommerce' ), 'WC_Meta_Box_Product_Data::output', 'product', 'normal', 'high' ); - add_meta_box( 'woocommerce-product-images', __( 'Product Gallery', 'woocommerce' ), 'WC_Meta_Box_Product_Images::output', 'product', 'side' ); + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; - // Orders - add_meta_box( 'woocommerce-order-data', __( 'Order Data', 'woocommerce' ), 'WC_Meta_Box_Order_Data::output', 'shop_order', 'normal', 'high' ); - add_meta_box( 'woocommerce-order-items', __( 'Order Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', 'shop_order', 'normal', 'high' ); - add_meta_box( 'woocommerce-order-totals', __( 'Order Totals', 'woocommerce' ), 'WC_Meta_Box_Order_Totals::output', 'shop_order', 'side', 'default' ); - add_meta_box( 'woocommerce-order-notes', __( 'Order Notes', 'woocommerce' ), 'WC_Meta_Box_Order_Notes::output', 'shop_order', 'side', 'default' ); - add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable Product Permissions', 'woocommerce' ) . ' [?]', 'WC_Meta_Box_Order_Downloads::output', 'shop_order', 'normal', 'default' ); - add_meta_box( 'woocommerce-order-actions', __( 'Order Actions', 'woocommerce' ), 'WC_Meta_Box_Order_Actions::output', 'shop_order', 'side', 'high' ); + // Products. + add_meta_box( 'postexcerpt', __( 'Product short description', 'woocommerce' ), 'WC_Meta_Box_Product_Short_Description::output', 'product', 'normal' ); + add_meta_box( 'woocommerce-product-data', __( 'Product data', 'woocommerce' ), 'WC_Meta_Box_Product_Data::output', 'product', 'normal', 'high' ); + add_meta_box( 'woocommerce-product-images', __( 'Product gallery', 'woocommerce' ), 'WC_Meta_Box_Product_Images::output', 'product', 'side', 'low' ); - // Coupons - add_meta_box( 'woocommerce-coupon-data', __( 'Coupon Data', 'woocommerce' ), 'WC_Meta_Box_Coupon_Data::output', 'shop_coupon', 'normal', 'high' ); + // Orders. + foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { + $order_type_object = get_post_type_object( $type ); + add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Data::output', $type, 'normal', 'high' ); + add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $type, 'normal', 'high' ); + add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Notes::output', $type, 'side', 'default' ); + add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $type, 'normal', 'default' ); + add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Actions::output', $type, 'side', 'high' ); + } - // Reviews - if ( 'comment' == get_current_screen()->id && isset( $_GET['c'] ) ) { - if ( get_comment_meta( intval( $_GET['c'] ), 'rating', true ) ) { - add_meta_box( 'woocommerce-rating', __( 'Rating', 'woocommerce' ), 'WC_Meta_Box_Order_Reviews::output', 'comment', 'normal', 'high' ); - } + // Coupons. + add_meta_box( 'woocommerce-coupon-data', __( 'Coupon data', 'woocommerce' ), 'WC_Meta_Box_Coupon_Data::output', 'shop_coupon', 'normal', 'high' ); + + // Comment rating. + if ( 'comment' === $screen_id && isset( $_GET['c'] ) && metadata_exists( 'comment', $_GET['c'], 'rating' ) ) { + add_meta_box( 'woocommerce-rating', __( 'Rating', 'woocommerce' ), 'WC_Meta_Box_Product_Reviews::output', 'comment', 'normal', 'high' ); } } /** - * Remove bloat + * Remove bloat. */ public function remove_meta_boxes() { remove_meta_box( 'postexcerpt', 'product', 'normal' ); remove_meta_box( 'product_shipping_classdiv', 'product', 'side' ); - remove_meta_box( 'pageparentdiv', 'product', 'side' ); - remove_meta_box( 'commentstatusdiv', 'product', 'normal' ); + remove_meta_box( 'commentsdiv', 'product', 'normal' ); remove_meta_box( 'commentstatusdiv', 'product', 'side' ); - remove_meta_box( 'woothemes-settings', 'shop_coupon' , 'normal' ); - remove_meta_box( 'commentstatusdiv', 'shop_coupon' , 'normal' ); - remove_meta_box( 'slugdiv', 'shop_coupon' , 'normal' ); - remove_meta_box( 'commentsdiv', 'shop_order' , 'normal' ); - remove_meta_box( 'woothemes-settings', 'shop_order' , 'normal' ); - remove_meta_box( 'commentstatusdiv', 'shop_order' , 'normal' ); - remove_meta_box( 'slugdiv', 'shop_order' , 'normal' ); + remove_meta_box( 'commentstatusdiv', 'product', 'normal' ); + remove_meta_box( 'woothemes-settings', 'shop_coupon', 'normal' ); + remove_meta_box( 'commentstatusdiv', 'shop_coupon', 'normal' ); + remove_meta_box( 'slugdiv', 'shop_coupon', 'normal' ); + + foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { + remove_meta_box( 'commentsdiv', $type, 'normal' ); + remove_meta_box( 'woothemes-settings', $type, 'normal' ); + remove_meta_box( 'commentstatusdiv', $type, 'normal' ); + remove_meta_box( 'slugdiv', $type, 'normal' ); + remove_meta_box( 'submitdiv', $type, 'side' ); + } } /** - * Rename core meta boxes + * Rename core meta boxes. */ public function rename_meta_boxes() { global $post; // Comments/Reviews - if ( isset( $post ) && ( 'publish' == $post->post_status || 'private' == $post->post_status ) ) { + if ( isset( $post ) && ( 'publish' == $post->post_status || 'private' == $post->post_status ) && post_type_supports( 'product', 'comments' ) ) { remove_meta_box( 'commentsdiv', 'product', 'normal' ); - add_meta_box( 'commentsdiv', __( 'Reviews', 'woocommerce' ), 'post_comment_meta_box', 'product', 'normal' ); } } /** - * Check if we're saving, the trigger an action based on the post type + * Check if we're saving, the trigger an action based on the post type. * * @param int $post_id * @param object $post */ public function save_meta_boxes( $post_id, $post ) { // $post_id and $post are required - if ( empty( $post_id ) || empty( $post ) ) { + if ( empty( $post_id ) || empty( $post ) || self::$saved_meta_boxes ) { return; } @@ -170,11 +190,11 @@ class WC_Admin_Meta_Boxes { if ( defined( 'DOING_AUTOSAVE' ) || is_int( wp_is_post_revision( $post ) ) || is_int( wp_is_post_autosave( $post ) ) ) { return; } - + // Check the nonce if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) { return; - } + } // Check the post being saved == the $post_id to prevent triggering this call for other save_post events if ( empty( $_POST['post_ID'] ) || $_POST['post_ID'] != $post_id ) { @@ -186,14 +206,19 @@ class WC_Admin_Meta_Boxes { return; } + // We need this save event to run once to avoid potential endless loops. This would have been perfect: + // remove_action( current_filter(), __METHOD__ ); + // But cannot be used due to https://github.com/woocommerce/woocommerce/issues/6485 + // When that is patched in core we can use the above. For now: + self::$saved_meta_boxes = true; + // Check the post type - if ( ! in_array( $post->post_type, array( 'product', 'shop_order', 'shop_coupon' ) ) ) { - return; + if ( in_array( $post->post_type, wc_get_order_types( 'order-meta-boxes' ) ) ) { + do_action( 'woocommerce_process_shop_order_meta', $post_id, $post ); + } elseif ( in_array( $post->post_type, array( 'product', 'shop_coupon' ) ) ) { + do_action( 'woocommerce_process_' . $post->post_type . '_meta', $post_id, $post ); } - - do_action( 'woocommerce_process_' . $post->post_type . '_meta', $post_id, $post ); } - } new WC_Admin_Meta_Boxes(); diff --git a/includes/admin/class-wc-admin-notices.php b/includes/admin/class-wc-admin-notices.php index 333caf9a613..d2455a51edc 100644 --- a/includes/admin/class-wc-admin-notices.php +++ b/includes/admin/class-wc-admin-notices.php @@ -1,123 +1,233 @@ callback. + * @var array + */ + private static $core_notices = array( + 'install' => 'install_notice', + 'update' => 'update_notice', + 'template_files' => 'template_file_check_notice', + 'theme_support' => 'theme_check_notice', + 'legacy_shipping' => 'legacy_shipping_notice', + 'no_shipping_methods' => 'no_shipping_methods_notice', + 'simplify_commerce' => 'simplify_commerce_notice', + ); + + /** + * Constructor. + */ + public static function init() { + self::$notices = get_option( 'woocommerce_admin_notices', array() ); + + add_action( 'switch_theme', array( __CLASS__, 'reset_admin_notices' ) ); + add_action( 'woocommerce_installed', array( __CLASS__, 'reset_admin_notices' ) ); + add_action( 'wp_loaded', array( __CLASS__, 'hide_notices' ) ); + add_action( 'shutdown', array( __CLASS__, 'store_notices' ) ); + + if ( current_user_can( 'manage_woocommerce' ) ) { + add_action( 'admin_print_styles', array( __CLASS__, 'add_notices' ) ); + } } /** - * Reset notices for themes when switched or a new version of WC is installed + * Store notices to DB */ - public function reset_admin_notices() { - update_option( 'woocommerce_admin_notices', array( 'template_files', 'theme_support' ) ); + public static function store_notices() { + update_option( 'woocommerce_admin_notices', self::get_notices() ); + } + + /** + * Get notices + * @return array + */ + public static function get_notices() { + return self::$notices; + } + + /** + * Remove all notices. + */ + public static function remove_all_notices() { + self::$notices = array(); + } + + /** + * Reset notices for themes when switched or a new version of WC is installed. + */ + public static function reset_admin_notices() { + if ( ! current_theme_supports( 'woocommerce' ) && ! in_array( get_option( 'template' ), wc_get_core_supported_themes() ) ) { + self::add_notice( 'theme_support' ); + } + + $simplify_options = get_option( 'woocommerce_simplify_commerce_settings', array() ); + $location = wc_get_base_location(); + + if ( ! class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) && ! empty( $simplify_options['enabled'] ) && 'yes' === $simplify_options['enabled'] && in_array( $location['country'], apply_filters( 'woocommerce_gateway_simplify_commerce_supported_countries', array( 'US', 'IE' ) ) ) ) { + WC_Admin_Notices::add_notice( 'simplify_commerce' ); + } + + self::add_notice( 'template_files' ); + } + + /** + * Show a notice. + * @param string $name + */ + public static function add_notice( $name ) { + self::$notices = array_unique( array_merge( self::get_notices(), array( $name ) ) ); + } + + /** + * Remove a notice from being displayed. + * @param string $name + */ + public static function remove_notice( $name ) { + self::$notices = array_diff( self::get_notices(), array( $name ) ); + delete_option( 'woocommerce_admin_notice_' . $name ); + } + + /** + * See if a notice is being shown. + * @param string $name + * @return boolean + */ + public static function has_notice( $name ) { + return in_array( $name, self::get_notices() ); + } + + /** + * Hide a notice if the GET variable is set. + */ + public static function hide_notices() { + if ( isset( $_GET['wc-hide-notice'] ) && isset( $_GET['_wc_notice_nonce'] ) ) { + if ( ! wp_verify_nonce( $_GET['_wc_notice_nonce'], 'woocommerce_hide_notices_nonce' ) ) { + wp_die( __( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( __( 'Cheatin’ huh?', 'woocommerce' ) ); + } + + $hide_notice = sanitize_text_field( $_GET['wc-hide-notice'] ); + self::remove_notice( $hide_notice ); + do_action( 'woocommerce_hide_' . $hide_notice . '_notice' ); + } } /** * Add notices + styles if needed. */ - public function add_notices() { - if ( get_option( '_wc_needs_update' ) == 1 || get_option( '_wc_needs_pages' ) == 1 ) { - wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ) ); - add_action( 'admin_notices', array( $this, 'install_notice' ) ); - } + public static function add_notices() { + $notices = self::get_notices(); - $notices = get_option( 'woocommerce_admin_notices', array() ); - - if ( ! empty( $_GET['hide_theme_support_notice'] ) ) { - $notices = array_diff( $notices, array( 'theme_support' ) ); - update_option( 'woocommerce_admin_notices', $notices ); - } - - if ( ! empty( $_GET['hide_template_files_notice'] ) ) { - $notices = array_diff( $notices, array( 'template_files' ) ); - update_option( 'woocommerce_admin_notices', $notices ); - } - - if ( in_array( 'theme_support', $notices ) && ! current_theme_supports( 'woocommerce' ) ) { - $template = get_option( 'template' ); - - if ( ! in_array( $template, wc_get_core_supported_themes() ) ) { - wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ) ); - add_action( 'admin_notices', array( $this, 'theme_check_notice' ) ); + if ( ! empty( $notices ) ) { + wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ) ); + foreach ( $notices as $notice ) { + if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'woocommerce_show_admin_notice', true, $notice ) ) { + add_action( 'admin_notices', array( __CLASS__, self::$core_notices[ $notice ] ) ); + } else { + add_action( 'admin_notices', array( __CLASS__, 'output_custom_notices' ) ); + } } } + } - if ( in_array( 'template_files', $notices ) ) { - wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ) ); - add_action( 'admin_notices', array( $this, 'template_file_check_notice' ) ); - } + /** + * Add a custom notice. + * @param string $name + * @param string $notice_html + */ + public static function add_custom_notice( $name, $notice_html ) { + self::add_notice( $name ); + update_option( 'woocommerce_admin_notice_' . $name, wp_kses_post( $notice_html ) ); + } - if ( in_array( 'translation_upgrade', $notices ) ) { - add_action( 'admin_notices', array( $this, 'translation_upgrade_notice' ) ); + /** + * Output any stored custom notices. + */ + public static function output_custom_notices() { + $notices = self::get_notices(); + + if ( ! empty( $notices ) ) { + foreach ( $notices as $notice ) { + if ( empty( self::$core_notices[ $notice ] ) ) { + $notice_html = get_option( 'woocommerce_admin_notice_' . $notice ); + + if ( $notice_html ) { + include( 'views/html-notice-custom.php' ); + } + } + } } } /** - * Show the install notices + * If we need to update, include a message with the update button. */ - public function install_notice() { - // If we need to update, include a message with the update button - if ( get_option( '_wc_needs_update' ) == 1 ) { - include( 'views/html-notice-update.php' ); - } - - // If we have just installed, show a message with the install pages button - elseif ( get_option( '_wc_needs_pages' ) == 1 ) { - include( 'views/html-notice-install.php' ); + public static function update_notice() { + if ( version_compare( get_option( 'woocommerce_db_version' ), WC_VERSION, '<' ) ) { + $updater = new WC_Background_Updater(); + if ( $updater->is_updating() || ! empty( $_GET['do_update_woocommerce'] ) ) { + include( 'views/html-notice-updating.php' ); + } else { + include( 'views/html-notice-update.php' ); + } + } else { + include( 'views/html-notice-updated.php' ); } } /** - * Show the Theme Check notice + * If we have just installed, show a message with the install pages button. */ - public function theme_check_notice() { - include( 'views/html-notice-theme-support.php' ); + public static function install_notice() { + include( 'views/html-notice-install.php' ); } /** - * Show the translation upgrade notice + * Show the Theme Check notice. */ - public function translation_upgrade_notice() { - $screen = get_current_screen(); - - if ( 'update-core' !== $screen->id ) { - include( 'views/html-notice-translation-upgrade.php' ); + public static function theme_check_notice() { + if ( ! current_theme_supports( 'woocommerce' ) && ! in_array( get_option( 'template' ), wc_get_core_supported_themes() ) ) { + include( 'views/html-notice-theme-support.php' ); + } else { + self::remove_notice( 'theme_support' ); } } /** - * Show a notice highlighting bad template files + * Show a notice highlighting bad template files. */ - public function template_file_check_notice() { - if ( isset( $_GET['page'] ) && 'wc-status' == $_GET['page'] ) { - return; - } - + public static function template_file_check_notice() { $core_templates = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates' ); $outdated = false; foreach ( $core_templates as $file ) { + $theme_file = false; if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . $file; @@ -125,11 +235,11 @@ class WC_Admin_Notices { $theme_file = get_stylesheet_directory() . '/woocommerce/' . $file; } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { $theme_file = get_template_directory() . '/' . $file; - } elseif( file_exists( get_template_directory() . '/woocommerce/' . $file ) ) { + } elseif ( file_exists( get_template_directory() . '/woocommerce/' . $file ) ) { $theme_file = get_template_directory() . '/woocommerce/' . $file; } - if ( $theme_file ) { + if ( false !== $theme_file ) { $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file ); $theme_version = WC_Admin_Status::get_file_version( $theme_file ); @@ -142,10 +252,64 @@ class WC_Admin_Notices { if ( $outdated ) { include( 'views/html-notice-template-check.php' ); + } else { + self::remove_notice( 'template_files' ); + } + } + + /** + * Show a notice asking users to convert to shipping zones. + */ + public static function legacy_shipping_notice() { + $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); + $enabled = false; + + foreach ( $maybe_load_legacy_methods as $method ) { + $options = get_option( 'woocommerce_' . $method . '_settings' ); + if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { + $enabled = true; + } + } + + if ( $enabled ) { + include( 'views/html-notice-legacy-shipping.php' ); + } else { + self::remove_notice( 'template_files' ); + } + } + + /** + * No shipping methods. + */ + public static function no_shipping_methods_notice() { + if ( wc_shipping_enabled() && ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) || 'wc-settings' !== $_GET['page'] || 'shipping' !== $_GET['tab'] ) ) { + $product_count = wp_count_posts( 'product' ); + $method_count = wc_get_shipping_method_count(); + + if ( $product_count->publish > 0 && 0 === $method_count ) { + include( 'views/html-notice-no-shipping-methods.php' ); + } + + if ( $method_count > 0 ) { + self::remove_notice( 'no_shipping_methods' ); + } + } + } + + /** + * Simplify Commerce is being removed from core. + */ + public static function simplify_commerce_notice() { + $location = wc_get_base_location(); + + if ( class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) || ! in_array( $location['country'], apply_filters( 'woocommerce_gateway_simplify_commerce_supported_countries', array( 'US', 'IE' ) ) ) ) { + self::remove_notice( 'simplify_commerce' ); + return; + } + if ( empty( $_GET['action'] ) ) { + include( 'views/html-notice-simplify-commerce.php' ); } } } -endif; - -return new WC_Admin_Notices(); +WC_Admin_Notices::init(); diff --git a/includes/admin/class-wc-admin-permalink-settings.php b/includes/admin/class-wc-admin-permalink-settings.php index acb7a43fa15..3614426cf00 100644 --- a/includes/admin/class-wc-admin-permalink-settings.php +++ b/includes/admin/class-wc-admin-permalink-settings.php @@ -1,22 +1,32 @@ permalinks = wc_get_permalink_structure(); } /** * Show a slug input box. */ public function product_category_slug_input() { - $permalinks = get_option( 'woocommerce_permalinks' ); ?> - + - + - /attribute-name/attribute/ + /attribute-name/attribute/ not using "default" permalinks above.', 'woocommerce' ) ); - - $permalinks = get_option( 'woocommerce_permalinks' ); - $product_permalink = $permalinks['product_base']; + echo wpautop( __( 'These settings control the permalinks used specifically for products.', 'woocommerce' ) ); // Get shop page - $shop_page_id = wc_get_page_id( 'shop' ); - $base_slug = ( $shop_page_id > 0 && get_page( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ); - $product_base = _x( 'product', 'default-slug', 'woocommerce' ); + $shop_page_id = wc_get_page_id( 'shop' ); + $base_slug = urldecode( ( $shop_page_id > 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ) ); + $product_base = _x( 'product', 'default-slug', 'woocommerce' ); $structures = array( 0 => '', - 1 => '/' . trailingslashit( $product_base ), - 2 => '/' . trailingslashit( $base_slug ), - 3 => '/' . trailingslashit( $base_slug ) . trailingslashit( '%product_cat%' ) + 1 => '/' . trailingslashit( $base_slug ), + 2 => '/' . trailingslashit( $base_slug ) . trailingslashit( '%product_cat%' ), ); ?> - +
    - - - - - - + + - - + + - - + + - + 0 && get_page( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ); + $shop_permalink = ( $shop_page_id > 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ); + if ( $shop_page_id && trim( $permalinks['product_base'], '/' ) === $shop_permalink ) { $permalinks['use_verbose_page_rules'] = true; } update_option( 'woocommerce_permalinks', $permalinks ); + wc_restore_locale(); } } } endif; -return new WC_Admin_Permalink_Settings(); \ No newline at end of file +return new WC_Admin_Permalink_Settings(); diff --git a/includes/admin/class-wc-admin-pointers.php b/includes/admin/class-wc-admin-pointers.php new file mode 100644 index 00000000000..187606c8001 --- /dev/null +++ b/includes/admin/class-wc-admin-pointers.php @@ -0,0 +1,262 @@ +id ) { + case 'product' : + $this->create_product_tutorial(); + break; + } + } + + /** + * Pointers for creating a product. + */ + public function create_product_tutorial() { + if ( ! isset( $_GET['tutorial'] ) || ! current_user_can( 'manage_options' ) ) { + return; + } + // These pointers will chain - they will not be shown at once. + $pointers = array( + 'pointers' => array( + 'title' => array( + 'target' => "#title", + 'next' => 'content', + 'next_trigger' => array( + 'target' => '#title', + 'event' => 'input', + ), + 'options' => array( + 'content' => '

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

    ', + 'position' => array( + 'edge' => 'right', + 'align' => 'middle', + ), + ), + ), + ), + ); + + $this->enqueue_pointers( $pointers ); + } + + /** + * Enqueue pointers and add script to page. + * @param array $pointers + */ + public function enqueue_pointers( $pointers ) { + $pointers = wp_json_encode( $pointers ); + wp_enqueue_style( 'wp-pointer' ); + wp_enqueue_script( 'wp-pointer' ); + wc_enqueue_js( " + jQuery( function( $ ) { + var wc_pointers = {$pointers}; + + setTimeout( init_wc_pointers, 800 ); + + function init_wc_pointers() { + $.each( wc_pointers.pointers, function( i ) { + show_wc_pointer( i ); + return false; + }); + } + + function show_wc_pointer( id ) { + var pointer = wc_pointers.pointers[ id ]; + var options = $.extend( pointer.options, { + close: function() { + if ( pointer.next ) { + show_wc_pointer( pointer.next ); + } + } + } ); + var this_pointer = $( pointer.target ).pointer( options ); + this_pointer.pointer( 'open' ); + + if ( pointer.next_trigger ) { + $( pointer.next_trigger.target ).on( pointer.next_trigger.event, function() { + setTimeout( function() { this_pointer.pointer( 'close' ); }, 400 ); + }); + } + } + }); + " ); + } +} + +new WC_Admin_Pointers(); diff --git a/includes/admin/class-wc-admin-post-types.php b/includes/admin/class-wc-admin-post-types.php index 16300f3a56d..a49d648a3e8 100644 --- a/includes/admin/class-wc-admin-post-types.php +++ b/includes/admin/class-wc-admin-post-types.php @@ -2,34 +2,42 @@ /** * Post Types Admin * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin - * @version 2.1.0 + * @author WooCommerce + * @category Admin + * @package WooCommerce/Admin + * @version 3.0.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; +} -if ( ! class_exists( 'WC_Admin_Post_Types' ) ) : +if ( ! class_exists( 'WC_Admin_Post_Types', false ) ) : /** - * WC_Admin_Post_Types Class + * WC_Admin_Post_Types Class. * * Handles the edit posts views and some functionality on the edit post screen for WC post types. */ class WC_Admin_Post_Types { /** - * Constructor + * Constructor. */ public function __construct() { add_filter( 'post_updated_messages', array( $this, 'post_updated_messages' ) ); + add_filter( 'bulk_post_updated_messages', array( $this, 'bulk_post_updated_messages' ), 10, 2 ); + + // Disable Auto Save add_action( 'admin_print_scripts', array( $this, 'disable_autosave' ) ); + // Extra post data. + add_action( 'edit_form_top', array( $this, 'edit_form_top' ) ); + // WP List table columns. Defined here so they are always available for events such as inline editing. - add_filter( 'manage_edit-product_columns', array( $this, 'product_columns' ) ); - add_filter( 'manage_edit-shop_coupon_columns', array( $this, 'shop_coupon_columns' ) ); - add_filter( 'manage_edit-shop_order_columns', array( $this, 'shop_order_columns' ) ); + add_filter( 'manage_product_posts_columns', array( $this, 'product_columns' ) ); + add_filter( 'manage_shop_coupon_posts_columns', array( $this, 'shop_coupon_columns' ) ); + add_filter( 'manage_shop_order_posts_columns', array( $this, 'shop_order_columns' ) ); add_action( 'manage_product_posts_custom_column', array( $this, 'render_product_columns' ), 2 ); add_action( 'manage_shop_coupon_posts_custom_column', array( $this, 'render_shop_coupon_columns' ), 2 ); @@ -39,16 +47,20 @@ class WC_Admin_Post_Types { add_filter( 'manage_edit-shop_coupon_sortable_columns', array( $this, 'shop_coupon_sortable_columns' ) ); add_filter( 'manage_edit-shop_order_sortable_columns', array( $this, 'shop_order_sortable_columns' ) ); - add_filter( 'bulk_actions-edit-shop_order', array( $this, 'shop_order_bulk_actions' ) ); + add_filter( 'list_table_primary_column', array( $this, 'list_table_primary_column' ), 10, 2 ); + add_filter( 'post_row_actions', array( $this, 'row_actions' ), 100, 2 ); - add_filter( 'views_edit-product', array( $this, 'product_sorting_link' ) ); + // Views + add_filter( 'views_edit-product', array( $this, 'product_views' ) ); + add_filter( 'disable_months_dropdown', array( $this, 'disable_months_dropdown' ), 10, 2 ); // Bulk / quick edit add_action( 'bulk_edit_custom_box', array( $this, 'bulk_edit' ), 10, 2 ); add_action( 'quick_edit_custom_box', array( $this, 'quick_edit' ), 10, 2 ); - add_action( 'save_post', array( $this, 'bulk_and_quick_edit_save_post' ), 10, 2 ); - add_action( 'admin_footer', array( $this, 'bulk_admin_footer' ), 10 ); - add_action( 'load-edit.php', array( $this, 'bulk_action' ) ); + add_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ), 10, 2 ); + add_action( 'woocommerce_product_bulk_and_quick_edit', array( $this, 'bulk_and_quick_edit_save_post' ), 10, 2 ); + add_filter( 'bulk_actions-edit-shop_order', array( $this, 'shop_order_bulk_actions' ) ); + add_filter( 'handle_bulk_actions-edit-shop_order', array( $this, 'handle_shop_order_bulk_actions' ), 10, 3 ); add_action( 'admin_notices', array( $this, 'bulk_admin_notices' ) ); // Order Search @@ -62,21 +74,12 @@ class WC_Admin_Post_Types { add_filter( 'parse_query', array( $this, 'product_filters_query' ) ); add_filter( 'posts_search', array( $this, 'product_search' ) ); - // Status transitions - add_action( 'delete_post', array( $this, 'delete_post' ) ); - add_action( 'wp_trash_post', array( $this, 'trash_post' ) ); - add_action( 'untrash_post', array( $this, 'untrash_post' ) ); - add_action( 'before_delete_post', array( $this, 'delete_order_items' ) ); - // Edit post screens - add_filter( 'media_view_strings', array( $this, 'media_view_strings' ), 10, 2 ); add_filter( 'enter_title_here', array( $this, 'enter_title_here' ), 1, 2 ); add_action( 'edit_form_after_title', array( $this, 'edit_form_after_title' ) ); - add_filter( 'media_view_strings', array( $this, 'change_insert_into_post' ) ); + add_filter( 'default_hidden_meta_boxes', array( $this, 'hidden_meta_boxes' ), 10, 2 ); add_action( 'post_submitbox_misc_actions', array( $this, 'product_data_visibility' ) ); - $this->change_featured_image_text(); - // Uploads add_filter( 'upload_dir', array( $this, 'upload_dir' ) ); add_action( 'media_upload_downloadable_product', array( $this, 'media_upload_downloadable_product' ) ); @@ -85,14 +88,165 @@ class WC_Admin_Post_Types { include( 'class-wc-admin-duplicate-product.php' ); } - include_once( 'class-wc-admin-meta-boxes.php' ); + include_once( dirname( __FILE__ ) . '/class-wc-admin-meta-boxes.php' ); - // Download permissions - add_action( 'woocommerce_process_product_file_download_paths', array( $this, 'process_product_file_download_paths' ), 10, 3 ); + // Disable DFW feature pointer + add_action( 'admin_footer', array( $this, 'disable_dfw_feature_pointer' ) ); + + // Disable post type view mode options + add_filter( 'view_mode_post_types', array( $this, 'disable_view_mode_options' ) ); + + // Update the screen options. + add_filter( 'default_hidden_columns', array( $this, 'adjust_shop_order_columns' ), 10, 2 ); + + // Show blank state + add_action( 'manage_posts_extra_tablenav', array( $this, 'maybe_render_blank_state' ) ); + + // Hide template for CPT archive. + add_filter( 'theme_page_templates', array( $this, 'hide_cpt_archive_templates' ), 10, 3 ); + add_action( 'edit_form_top', array( $this, 'show_cpt_archive_notice' ) ); + + // Add a post display state for special WC pages. + add_filter( 'display_post_states', array( $this, 'add_display_post_states' ), 10, 2 ); } /** - * Define custom columns for products + * Adjust shop order columns for the user on certain conditions. + * + * @param array $hidden + * @param object $screen + * + * @return array + */ + public function adjust_shop_order_columns( $hidden, $screen ) { + if ( isset( $screen->id ) && 'edit-shop_order' === $screen->id ) { + if ( 'disabled' === get_option( 'woocommerce_ship_to_countries' ) ) { + $hidden[] = 'shipping_address'; + } else { + $hidden[] = 'billing_address'; + } + } + return $hidden; + } + + /** + * Change messages when a post type is updated. + * @param array $messages + * @return array + */ + public function post_updated_messages( $messages ) { + global $post, $post_ID; + + $messages['product'] = array( + 0 => '', // Unused. Messages start at index 1. + 1 => sprintf( __( 'Product updated. View Product', 'woocommerce' ), esc_url( get_permalink( $post_ID ) ) ), + 2 => __( 'Custom field updated.', 'woocommerce' ), + 3 => __( 'Custom field deleted.', 'woocommerce' ), + 4 => __( 'Product updated.', 'woocommerce' ), + /* translators: %s: revision title */ + 5 => isset( $_GET['revision'] ) ? sprintf( __( 'Product restored to revision from %s', 'woocommerce' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, + /* translators: %s: product url */ + 6 => sprintf( __( 'Product published. View Product', 'woocommerce' ), esc_url( get_permalink( $post_ID ) ) ), + 7 => __( 'Product saved.', 'woocommerce' ), + /* translators: %s: product url */ + 8 => sprintf( __( 'Product submitted. Preview product', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) ), + /* translators: 1: date 2: product url */ + 9 => sprintf( __( 'Product scheduled for: %1$s. Preview product', 'woocommerce' ), + date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ), esc_url( get_permalink( $post_ID ) ) ), + /* translators: %s: product url */ + 10 => sprintf( __( 'Product draft updated. Preview product', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) ), + ); + + $messages['shop_order'] = array( + 0 => '', // Unused. Messages start at index 1. + 1 => __( 'Order updated.', 'woocommerce' ), + 2 => __( 'Custom field updated.', 'woocommerce' ), + 3 => __( 'Custom field deleted.', 'woocommerce' ), + 4 => __( 'Order updated.', 'woocommerce' ), + /* translators: %s: revision title */ + 5 => isset( $_GET['revision'] ) ? sprintf( __( 'Order restored to revision from %s', 'woocommerce' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, + 6 => __( 'Order updated.', 'woocommerce' ), + 7 => __( 'Order saved.', 'woocommerce' ), + 8 => __( 'Order submitted.', 'woocommerce' ), + /* translators: %s: date */ + 9 => sprintf( __( 'Order scheduled for: %1$s.', 'woocommerce' ), + date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) ), + 10 => __( 'Order draft updated.', 'woocommerce' ), + 11 => __( 'Order updated and sent to the customer.', 'woocommerce' ), + ); + + $messages['shop_coupon'] = array( + 0 => '', // Unused. Messages start at index 1. + 1 => __( 'Coupon updated.', 'woocommerce' ), + 2 => __( 'Custom field updated.', 'woocommerce' ), + 3 => __( 'Custom field deleted.', 'woocommerce' ), + 4 => __( 'Coupon updated.', 'woocommerce' ), + /* translators: %s: revision title */ + 5 => isset( $_GET['revision'] ) ? sprintf( __( 'Coupon restored to revision from %s', 'woocommerce' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, + 6 => __( 'Coupon updated.', 'woocommerce' ), + 7 => __( 'Coupon saved.', 'woocommerce' ), + 8 => __( 'Coupon submitted.', 'woocommerce' ), + /* translators: %s: date */ + 9 => sprintf( __( 'Coupon scheduled for: %1$s.', 'woocommerce' ), + date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) ), + 10 => __( 'Coupon draft updated.', 'woocommerce' ), + ); + + return $messages; + } + + /** + * Specify custom bulk actions messages for different post types. + * @param array $bulk_messages + * @param array $bulk_counts + * @return array + */ + public function bulk_post_updated_messages( $bulk_messages, $bulk_counts ) { + + $bulk_messages['product'] = array( + /* translators: %s: product count */ + 'updated' => _n( '%s product updated.', '%s products updated.', $bulk_counts['updated'], 'woocommerce' ), + /* translators: %s: product count */ + 'locked' => _n( '%s product not updated, somebody is editing it.', '%s products not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), + /* translators: %s: product count */ + 'deleted' => _n( '%s product permanently deleted.', '%s products permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), + /* translators: %s: product count */ + 'trashed' => _n( '%s product moved to the Trash.', '%s products moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), + /* translators: %s: product count */ + 'untrashed' => _n( '%s product restored from the Trash.', '%s products restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), + ); + + $bulk_messages['shop_order'] = array( + /* translators: %s: order count */ + 'updated' => _n( '%s order updated.', '%s orders updated.', $bulk_counts['updated'], 'woocommerce' ), + /* translators: %s: order count */ + 'locked' => _n( '%s order not updated, somebody is editing it.', '%s orders not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), + /* translators: %s: order count */ + 'deleted' => _n( '%s order permanently deleted.', '%s orders permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), + /* translators: %s: order count */ + 'trashed' => _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), + /* translators: %s: order count */ + 'untrashed' => _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), + ); + + $bulk_messages['shop_coupon'] = array( + /* translators: %s: coupon count */ + 'updated' => _n( '%s coupon updated.', '%s coupons updated.', $bulk_counts['updated'], 'woocommerce' ), + /* translators: %s: coupon count */ + 'locked' => _n( '%s coupon not updated, somebody is editing it.', '%s coupons not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), + /* translators: %s: coupon count */ + 'deleted' => _n( '%s coupon permanently deleted.', '%s coupons permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), + /* translators: %s: coupon count */ + 'trashed' => _n( '%s coupon moved to the Trash.', '%s coupons moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), + /* translators: %s: coupon count */ + 'untrashed' => _n( '%s coupon restored from the Trash.', '%s coupons restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), + ); + + return $bulk_messages; + } + + /** + * Define custom columns for products. * @param array $existing_columns * @return array */ @@ -105,60 +259,61 @@ class WC_Admin_Post_Types { $columns = array(); $columns['cb'] = ''; - $columns['thumb'] = '' . __( 'Image', 'woocommerce' ) . ''; + $columns['thumb'] = '' . __( 'Image', 'woocommerce' ) . ''; $columns['name'] = __( 'Name', 'woocommerce' ); if ( wc_product_sku_enabled() ) { $columns['sku'] = __( 'SKU', 'woocommerce' ); } - if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { $columns['is_in_stock'] = __( 'Stock', 'woocommerce' ); } $columns['price'] = __( 'Price', 'woocommerce' ); $columns['product_cat'] = __( 'Categories', 'woocommerce' ); $columns['product_tag'] = __( 'Tags', 'woocommerce' ); - $columns['featured'] = '' . __( 'Featured', 'woocommerce' ) . ''; - $columns['product_type'] = '' . __( 'Type', 'woocommerce' ) . ''; + $columns['featured'] = '' . __( 'Featured', 'woocommerce' ) . ''; + $columns['product_type'] = '' . __( 'Type', 'woocommerce' ) . ''; $columns['date'] = __( 'Date', 'woocommerce' ); return array_merge( $columns, $existing_columns ); + } /** - * Define custom columns for coupons + * Define custom columns for coupons. * @param array $existing_columns * @return array */ public function shop_coupon_columns( $existing_columns ) { $columns = array(); - $columns["cb"] = ""; - $columns["coupon_code"] = __( 'Code', 'woocommerce' ); - $columns["type"] = __( 'Coupon type', 'woocommerce' ); - $columns["amount"] = __( 'Coupon amount', 'woocommerce' ); - $columns["description"] = __( 'Description', 'woocommerce' ); - $columns["products"] = __( 'Product IDs', 'woocommerce' ); - $columns["usage"] = __( 'Usage / Limit', 'woocommerce' ); - $columns["expiry_date"] = __( 'Expiry date', 'woocommerce' ); + $columns['cb'] = $existing_columns['cb']; + $columns['coupon_code'] = __( 'Code', 'woocommerce' ); + $columns['type'] = __( 'Coupon type', 'woocommerce' ); + $columns['amount'] = __( 'Coupon amount', 'woocommerce' ); + $columns['description'] = __( 'Description', 'woocommerce' ); + $columns['products'] = __( 'Product IDs', 'woocommerce' ); + $columns['usage'] = __( 'Usage / Limit', 'woocommerce' ); + $columns['expiry_date'] = __( 'Expiry date', 'woocommerce' ); return $columns; } /** - * Define custom columns for orders + * Define custom columns for orders. * @param array $existing_columns * @return array */ public function shop_order_columns( $existing_columns ) { $columns = array(); - $columns['cb'] = ''; + $columns['cb'] = $existing_columns['cb']; $columns['order_status'] = '' . esc_attr__( 'Status', 'woocommerce' ) . ''; $columns['order_title'] = __( 'Order', 'woocommerce' ); - $columns['order_items'] = __( 'Purchased', 'woocommerce' ); + $columns['billing_address'] = __( 'Billing', 'woocommerce' ); $columns['shipping_address'] = __( 'Ship to', 'woocommerce' ); - $columns['customer_message'] = '' . esc_attr__( 'Customer Message', 'woocommerce' ) . ''; - $columns['order_notes'] = '' . esc_attr__( 'Order Notes', 'woocommerce' ) . ''; + $columns['customer_message'] = '' . esc_attr__( 'Customer message', 'woocommerce' ) . ''; + $columns['order_notes'] = '' . esc_attr__( 'Order notes', 'woocommerce' ) . ''; $columns['order_date'] = __( 'Date', 'woocommerce' ); $columns['order_total'] = __( 'Total', 'woocommerce' ); $columns['order_actions'] = __( 'Actions', 'woocommerce' ); @@ -167,34 +322,38 @@ class WC_Admin_Post_Types { } /** - * Ouput custom columns for products - * @param string $column + * Ouput custom columns for products. + * + * @param string $column */ public function render_product_columns( $column ) { - global $post; + global $post, $the_product; - if ( empty( $the_product ) || $the_product->id != $post->ID ) { - $the_product = get_product( $post ); + if ( empty( $the_product ) || $the_product->get_id() != $post->ID ) { + $the_product = wc_get_product( $post ); + } + + // Only continue if we have a product. + if ( empty( $the_product ) ) { + return; } switch ( $column ) { case 'thumb' : - echo '' . $the_product->get_image() . ''; + echo '' . $the_product->get_image( 'thumbnail' ) . ''; break; case 'name' : - $edit_link = get_edit_post_link( $post->ID ); - $title = _draft_or_post_title(); - $post_type_object = get_post_type_object( $post->post_type ); - $can_edit_post = current_user_can( $post_type_object->cap->edit_post, $post->ID ); + $edit_link = get_edit_post_link( $post->ID ); + $title = _draft_or_post_title(); - echo '' . $title.''; + echo '' . esc_html( $title ) . ''; _post_states( $post ); echo ''; if ( $post->post_parent > 0 ) { - echo '  ← '. get_the_title( $post->post_parent ) .''; + echo '  ← ' . get_the_title( $post->post_parent ) . ''; } // Excerpt view @@ -202,99 +361,56 @@ class WC_Admin_Post_Types { echo apply_filters( 'the_excerpt', $post->post_excerpt ); } - // Get actions - $actions = array(); - - $actions['id'] = 'ID: ' . $post->ID; - - if ( $can_edit_post && 'trash' != $post->post_status ) { - $actions['edit'] = '' . __( 'Edit', 'woocommerce' ) . ''; - $actions['inline hide-if-no-js'] = '' . __( 'Quick Edit', 'woocommerce' ) . ''; - } - if ( current_user_can( $post_type_object->cap->delete_post, $post->ID ) ) { - if ( 'trash' == $post->post_status ) { - $actions['untrash'] = '' . __( 'Restore', 'woocommerce' ) . ''; - } elseif ( EMPTY_TRASH_DAYS ) { - $actions['trash'] = '' . __( 'Trash', 'woocommerce' ) . ''; - } - - if ( 'trash' == $post->post_status || ! EMPTY_TRASH_DAYS ) { - $actions['delete'] = '' . __( 'Delete Permanently', 'woocommerce' ) . ''; - } - } - if ( $post_type_object->public ) { - if ( in_array( $post->post_status, array( 'pending', 'draft', 'future' ) ) ) { - if ( $can_edit_post ) - $actions['view'] = '' . __( 'Preview', 'woocommerce' ) . ''; - } elseif ( 'trash' != $post->post_status ) { - $actions['view'] = '' . __( 'View', 'woocommerce' ) . ''; - } - } - - $actions = apply_filters( 'post_row_actions', $actions, $post ); - - echo '
    '; - - $i = 0; - $action_count = sizeof($actions); - - foreach ( $actions as $action => $link ) { - ++$i; - ( $i == $action_count ) ? $sep = '' : $sep = ' | '; - echo '' . $link . $sep . ''; - } - echo '
    '; - get_inline_data( $post ); - /* Custom inline data for woocommerce */ + /* Custom inline data for woocommerce. */ echo ' - '; + } + + /** + * Introduction step. + */ + public function wc_setup_introduction() { + ?> +

    +

    It’s completely optional and shouldn’t take longer than five minutes.', 'woocommerce' ); ?>

    +

    +

    + + +

    + +

    +
    +

    pages. The following will be created automatically (if they do not already exist):', 'woocommerce' ), esc_url( admin_url( 'edit.php?post_type=page' ) ) ); ?>

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

    Pages screen. You can control which pages are shown on your website via Appearance > Menus.', 'woocommerce' ), esc_url( admin_url( 'edit.php?post_type=page' ) ), esc_url( admin_url( 'nav-menus.php' ) ) ); ?>

    + +

    + + + +

    +
    + get_next_step_link() ) ); + exit; + } + + /** + * Location and Tax settings. + */ + public function wc_setup_location() { + $address = WC()->countries->get_base_address(); + $address_2 = WC()->countries->get_base_address_2(); + $city = WC()->countries->get_base_city(); + $state = WC()->countries->get_base_state(); + $country = WC()->countries->get_base_country(); + $postcode = WC()->countries->get_base_postcode(); + + if ( empty( $country ) ) { + $user_location = WC_Geolocation::geolocate_ip(); + $country = ! empty( $user_location['country'] ) ? $user_location['country'] : 'US'; + $state = ! empty( $user_location['state'] ) ? $user_location['state'] : '*'; + $state = 'US' === $country && '*' === $state ? 'AL' : $state; + } + + // Defaults + $currency = get_option( 'woocommerce_currency', 'GBP' ); + $currency_pos = get_option( 'woocommerce_currency_pos', 'left' ); + $decimal_sep = get_option( 'woocommerce_price_decimal_sep', '.' ); + $num_decimals = get_option( 'woocommerce_price_num_decimals', '2' ); + $thousand_sep = get_option( 'woocommerce_price_thousand_sep', ',' ); + ?> +

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
      + + +
      + + +
      + + +
      + + +
    + + add it later.', 'woocommerce' ), 'https://docs.woocommerce.com/document/add-a-custom-currency-symbol/' ); ?> +
    + +
    + +
    + +
    + +
    + id="woocommerce_calc_taxes" name="woocommerce_calc_taxes" class="input-checkbox" value="1" /> + +
    +
    + +
    +

    + + + +

    +
    + plugin_path() . '/i18n/locale-info.php' ); + $tax_rates = array(); + $country = WC()->countries->get_base_country(); + $state = WC()->countries->get_base_state(); + + if ( isset( $locale_info[ $country ] ) ) { + if ( isset( $locale_info[ $country ]['tax_rates'][ $state ] ) ) { + $tax_rates = $locale_info[ $country ]['tax_rates'][ $state ]; + } elseif ( isset( $locale_info[ $country ]['tax_rates'][''] ) ) { + $tax_rates = $locale_info[ $country ]['tax_rates']['']; + } + if ( isset( $locale_info[ $country ]['tax_rates']['*'] ) ) { + $tax_rates = array_merge( $locale_info[ $country ]['tax_rates']['*'], $tax_rates ); + } + } + if ( $tax_rates ) { + $loop = 0; + foreach ( $tax_rates as $rate ) { + $tax_rate = array( + 'tax_rate_country' => $rate['country'], + 'tax_rate_state' => $rate['state'], + 'tax_rate' => $rate['rate'], + 'tax_rate_name' => $rate['name'], + 'tax_rate_priority' => isset( $rate['priority'] ) ? absint( $rate['priority'] ) : 1, + 'tax_rate_compound' => 0, + 'tax_rate_shipping' => $rate['shipping'] ? 1 : 0, + 'tax_rate_order' => $loop ++, + 'tax_rate_class' => '', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + } + } + } + + wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); + exit; + } + + /** + * Tout WooCommerce Services for North American stores. + */ + protected function wc_setup_wcs_tout() { + $base_location = wc_get_base_location(); + + if ( false === $base_location['country'] ) { + $base_location = WC_Geolocation::geolocate_ip(); + } + + if ( ! in_array( $base_location['country'], array( 'US', 'CA' ), true ) ) { + return; + } + + $default_content = array( + 'title' => __( 'Enable WooCommerce Shipping (recommended)', 'woocommerce' ), + 'description' => __( 'Print labels and get discounted USPS shipping rates, right from your WooCommerce dashboard. Powered by WooCommerce Services.', 'woocommerce' ), + ); + + switch ( $base_location['country'] ) { + case 'CA': + $local_content = array( + 'title' => __( 'Enable WooCommerce Shipping (recommended)', 'woocommerce' ), + 'description' => __( 'Display live rates from Canada Post at checkout to make shipping a breeze. Powered by WooCommerce Services.', 'woocommerce' ), + ); + break; + default: + $local_content = array(); + } + + $content = wp_parse_args( $local_content, $default_content ); + ?> +
      +
    • +
      + + +
      +
      +

      + +

      +
      +
    • +
    + +

    +
    + wc_setup_wcs_tout(); ?> + + + + + + + + + + +
    + +
    + +
    + +

    + + + +

    +
    + get_next_step_link() ) ); + exit; + } + + $current_shipping = get_option( 'woocommerce_ship_to_countries' ); + $install_services = isset( $_POST['woocommerce_install_services'] ); + $weight_unit = sanitize_text_field( $_POST['weight_unit'] ); + $dimension_unit = sanitize_text_field( $_POST['dimension_unit'] ); + + update_option( 'woocommerce_ship_to_countries', '' ); + update_option( 'woocommerce_weight_unit', $weight_unit ); + update_option( 'woocommerce_dimension_unit', $dimension_unit ); + + /* + * If this is the initial shipping setup, create a shipping + * zone containing the country the store is located in, with + * a "free shipping" method preconfigured. + */ + if ( false === $current_shipping ) { + $default_country = get_option( 'woocommerce_default_country' ); + $location = wc_format_country_state_string( $default_country ); + + $zone = new WC_Shipping_Zone( null ); + $zone->set_zone_order( 0 ); + $zone->add_location( $location['country'], 'country' ); + $zone->set_zone_name( $zone->get_formatted_location() ); + $zone->add_shipping_method( 'free_shipping' ); + $zone->save(); + } + + if ( $install_services && ! is_plugin_active( 'woocommerce-services/woocommerce-services.php' ) ) { + $services_plugin_id = 'woocommerce-services'; + $services_plugin = array( + 'name' => __( 'WooCommerce Services', 'woocommerce' ), + 'repo-slug' => 'woocommerce-services', + ); + wp_schedule_single_event( time() + 10, 'woocommerce_plugin_background_installer', array( $services_plugin_id, $services_plugin ) ); + } else { + WC_Admin_Notices::add_notice( 'no_shipping_methods' ); + } + + wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); + exit; + } + + /** + * Simple array of gateways to show in wizard. + * @return array + */ + protected function get_wizard_payment_gateways() { + $gateways = array( + 'paypal-braintree' => array( + 'name' => __( 'PayPal by Braintree', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/paypal-braintree.png', + 'description' => __( "Safe and secure payments using credit cards or your customer's PayPal account.", 'woocommerce' ) . ' ' . __( 'Learn more about PayPal', 'woocommerce' ) . '', + 'class' => 'featured featured-row-last', + 'repo-slug' => 'woocommerce-gateway-paypal-powered-by-braintree', + ), + 'paypal-ec' => array( + 'name' => __( 'PayPal Express Checkout', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/paypal.png', + 'description' => __( "Safe and secure payments using credit cards or your customer's PayPal account.", 'woocommerce' ) . ' ' . __( 'Learn more about PayPal', 'woocommerce' ) . '', + 'class' => 'featured featured-row-last', + 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', + ), + 'stripe' => array( + 'name' => __( 'Stripe', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/stripe.png', + 'description' => sprintf( __( 'A modern and robust way to accept credit card payments on your store. Learn more about Stripe.', 'woocommerce' ), 'https://wordpress.org/plugins/woocommerce-gateway-stripe/' ), + 'class' => 'featured featured-row-first', + 'repo-slug' => 'woocommerce-gateway-stripe', + ), + 'paypal' => array( + 'name' => __( 'PayPal Standard', 'woocommerce' ), + 'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ), + 'image' => '', + 'class' => '', + 'settings' => array( + 'email' => array( + 'label' => __( 'PayPal email address', 'woocommerce' ), + 'type' => 'email', + 'value' => get_option( 'admin_email' ), + 'placeholder' => __( 'PayPal email address', 'woocommerce' ), + ), + ), + ), + 'cheque' => array( + 'name' => _x( 'Check payments', 'Check payment method', 'woocommerce' ), + 'description' => __( 'A simple offline gateway that lets you accept a check as method of payment.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + 'bacs' => array( + 'name' => __( 'Bank transfer (BACS) payments', 'woocommerce' ), + 'description' => __( 'A simple offline gateway that lets you accept BACS payment.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + 'cod' => array( + 'name' => __( 'Cash on delivery', 'woocommerce' ), + 'description' => __( 'A simple offline gateway that lets you accept cash on delivery.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + ); + + $country = WC()->countries->get_base_country(); + + if ( 'US' === $country ) { + unset( $gateways['paypal-ec'] ); + } else { + unset( $gateways['paypal-braintree'] ); + } + + if ( ! current_user_can( 'install_plugins' ) ) { + unset( $gateways['paypal-braintree'] ); + unset( $gateways['paypal-ec'] ); + unset( $gateways['stripe'] ); + } + + return $gateways; + } + + /** + * Payments Step. + */ + public function wc_setup_payments() { + $gateways = $this->get_wizard_payment_gateways(); + ?> +

    +
    +

    Additional payment methods can be installed later and managed from the checkout settings screen.', 'woocommerce' ), esc_url( admin_url( 'admin.php?page=wc-addons&view=payment-gateways' ) ), esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout' ) ) ); ?>

    + +
      + $gateway ) : ?> +
    • +
      + + +
      +
      + +
      + + + $setting ) : ?> + + + + + +
      + +
      + +
    • + +
    +

    + + + +

    +
    + get_wizard_payment_gateways(); + + foreach ( $gateways as $gateway_id => $gateway ) { + // If repo-slug is defined, download and install plugin from .org. + if ( ! empty( $gateway['repo-slug'] ) && ! empty( $_POST[ 'wc-wizard-gateway-' . $gateway_id . '-enabled' ] ) ) { + wp_schedule_single_event( time() + 10, 'woocommerce_plugin_background_installer', array( $gateway_id, $gateway ) ); + } + + $settings_key = 'woocommerce_' . $gateway_id . '_settings'; + $settings = array_filter( (array) get_option( $settings_key, array() ) ); + $settings['enabled'] = ! empty( $_POST[ 'wc-wizard-gateway-' . $gateway_id . '-enabled' ] ) ? 'yes' : 'no'; + + if ( ! empty( $gateway['settings'] ) ) { + foreach ( $gateway['settings'] as $setting_id => $setting ) { + $settings[ $setting_id ] = wc_clean( $_POST[ $gateway_id . '_' . $setting_id ] ); + } + } + + update_option( $settings_key, $settings ); + } + + wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); + exit; + } + + /** + * Theme step. + */ + private function wc_setup_theme() { + ?> +
    +

    + Storefront is the free WordPress theme built and maintained by the makers of WooCommerce.', 'woocommerce' ) ); ?> + Storefront +

    + +
      +
    • Bulletproof WooCommerce integration: Rest assured the integration between WooCommerce, WooCommerce extensions and Storefront is water-tight.', 'woocommerce' ) ); ?>
    • +
    • Built with accessibility in mind: Storefront adheres to the strict wordpress.org accessibility guidelines making your store accessible to the widest audience possible.', 'woocommerce' ) ); ?>
    • +
    • Child themes and extensions available: Like WooCommerce, you can extend Storefront with an extension or child theme to make your store truly your own.', 'woocommerce' ) ); ?>
    • +
    • No Shortcodes, sliders or page builders: Bring your favorite sliders or page builders, Storefront is built to work with the most popular options.', 'woocommerce' ) ); ?>
    • +
    • Clean, simple mobile-first design: The perfect place to start when customizing your store, looks beautiful on any device.', 'woocommerce' ) ); ?>
    • + +
    +

    + + + +

    +
    + get_next_step_link() ) ); + exit; + } + + /** + * Actions on the final step. + */ + private function wc_setup_ready_actions() { + WC_Admin_Notices::remove_notice( 'install' ); + + if ( isset( $_GET['wc_tracker_optin'] ) && isset( $_GET['wc_tracker_nonce'] ) && wp_verify_nonce( $_GET['wc_tracker_nonce'], 'wc_tracker_optin' ) ) { + update_option( 'woocommerce_allow_tracking', 'yes' ); + WC_Tracker::send_tracking_data( true ); + + } elseif ( isset( $_GET['wc_tracker_optout'] ) && isset( $_GET['wc_tracker_nonce'] ) && wp_verify_nonce( $_GET['wc_tracker_nonce'], 'wc_tracker_optout' ) ) { + update_option( 'woocommerce_allow_tracking', 'no' ); + } + } + + /** + * Final step. + */ + public function wc_setup_ready() { + $this->wc_setup_ready_actions(); + shuffle( $this->tweets ); + ?> + + + +

    + + +
    +

    ', '' ); ?>

    +

    + + +

    +
    + + +
    +
    +

    +
      +
    • +
    • +
    +
    +
    +

    +
      +
    • + +
    • +
    +
    +
    + execute_tool( $action ); + } else { + $response = array( 'success' => false, 'message' => __( 'Tool does not exist.', 'woocommerce' ) ); + } - echo '

    ' . __( 'Product Transients Cleared', 'woocommerce' ) . '

    '; - break; - case 'clear_expired_transients' : - - // http://w-shadow.com/blog/2012/04/17/delete-stale-transients/ - $rows = $wpdb->query( " - DELETE - a, b - FROM - {$wpdb->options} a, {$wpdb->options} b - WHERE - a.option_name LIKE '_transient_%' AND - a.option_name NOT LIKE '_transient_timeout_%' AND - b.option_name = CONCAT( - '_transient_timeout_', - SUBSTRING( - a.option_name, - CHAR_LENGTH('_transient_') + 1 - ) - ) - AND b.option_value < UNIX_TIMESTAMP() - " ); - - $rows2 = $wpdb->query( " - DELETE - a, b - FROM - {$wpdb->options} a, {$wpdb->options} b - WHERE - a.option_name LIKE '_site_transient_%' AND - a.option_name NOT LIKE '_site_transient_timeout_%' AND - b.option_name = CONCAT( - '_site_transient_timeout_', - SUBSTRING( - a.option_name, - CHAR_LENGTH('_site_transient_') + 1 - ) - ) - AND b.option_value < UNIX_TIMESTAMP() - " ); - - echo '

    ' . sprintf( __( '%d Transients Rows Cleared', 'woocommerce' ), $rows + $rows2 ) . '

    '; - - break; - case 'reset_roles' : - // Remove then re-add caps and roles - $installer = include( WC()->plugin_path() . '/includes/class-wc-install.php' ); - $installer->remove_roles(); - $installer->create_roles(); - - echo '

    ' . __( 'Roles successfully reset', 'woocommerce' ) . '

    '; - break; - case 'recount_terms' : - - $product_cats = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'id=>parent' ) ); - - _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false ); - - $product_tags = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'id=>parent' ) ); - - _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false ); - - echo '

    ' . __( 'Terms successfully recounted', 'woocommerce' ) . '

    '; - break; - case 'clear_sessions' : - - $wpdb->query( " - DELETE FROM {$wpdb->options} - WHERE option_name LIKE '_wc_session_%' OR option_name LIKE '_wc_session_expires_%' - " ); - - wp_cache_flush(); - - echo '

    ' . __( 'Sessions successfully cleared', 'woocommerce' ) . '

    '; - break; - case 'install_pages' : - WC_Install::create_pages(); - echo '

    ' . __( 'All missing WooCommerce pages was installed successfully.', 'woocommerce' ) . '

    '; - break; - case 'delete_taxes' : - - $wpdb->query( "TRUNCATE " . $wpdb->prefix . "woocommerce_tax_rates" ); - - $wpdb->query( "TRUNCATE " . $wpdb->prefix . "woocommerce_tax_rate_locations" ); - - echo '

    ' . __( 'Tax rates successfully deleted', 'woocommerce' ) . '

    '; - break; - case 'translation_upgrade' : - // Delete language pack version - delete_option( 'woocommerce_language_pack_version' ); - - // Force WordPress find for new updates - set_site_transient( 'update_plugins', null ); - - echo '

    ' . sprintf( __( 'Forced the translations upgrade successfully, please check the Updates page', 'woocommerce' ), add_query_arg( array( 'force-check' => '1' ), admin_url( 'update-core.php' ) ) ) . '

    '; - break; - default : - $action = esc_attr( $_GET['action'] ); - if ( isset( $tools[ $action ]['callback'] ) ) { - $callback = $tools[ $action ]['callback']; - $return = call_user_func( $callback ); - if ( $return === false ) { - if ( is_array( $callback ) ) { - echo '

    ' . sprintf( __( 'There was an error calling %s::%s', 'woocommerce' ), get_class( $callback[0] ), $callback[1] ) . '

    '; - - } else { - echo '

    ' . sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback ) . '

    '; - } - } - } - break; + if ( $response['success'] ) { + echo '

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

    '; + } else { + echo '

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

    '; } } // Display message if settings settings have been saved if ( isset( $_REQUEST['settings-updated'] ) ) { - echo '

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

    '; + echo '

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

    '; } - include_once( 'views/html-admin-page-status-tools.php' ); + include_once( dirname( __FILE__ ) . '/views/html-admin-page-status-tools.php' ); } /** - * Get tools - * + * Get tools. * @return array of tools */ public static function get_tools() { - $tools = array( - 'clear_transients' => array( - 'name' => __( 'WC Transients','woocommerce'), - 'button' => __('Clear transients','woocommerce'), - 'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ), - ), - 'clear_expired_transients' => array( - 'name' => __( 'Expired Transients','woocommerce'), - 'button' => __('Clear expired transients','woocommerce'), - 'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ), - ), - 'recount_terms' => array( - 'name' => __('Term counts','woocommerce'), - 'button' => __('Recount terms','woocommerce'), - 'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ), - ), - 'reset_roles' => array( - 'name' => __('Capabilities','woocommerce'), - 'button' => __('Reset capabilities','woocommerce'), - 'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ), - ), - 'clear_sessions' => array( - 'name' => __('Customer Sessions','woocommerce'), - 'button' => __('Clear all sessions','woocommerce'), - 'desc' => __( 'Warning: This tool will delete all customer session data from the database, including any current live carts.', 'woocommerce' ), - ), - 'install_pages' => array( - 'name' => __( 'Install WooCommerce Pages', 'woocommerce' ), - 'button' => __( 'Install pages', 'woocommerce' ), - 'desc' => __( 'Note: This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' ), - ), - 'delete_taxes' => array( - 'name' => __( 'Delete all WooCommerce tax rates', 'woocommerce' ), - 'button' => __( 'Delete ALL tax rates', 'woocommerce' ), - 'desc' => __( 'Note: This option will delete ALL of your tax rates, use with caution.', 'woocommerce' ), - ) - ); - - if ( defined( 'WPLANG' ) && '' !== WPLANG ) { - $tools['translation_upgrade'] = array( - 'name' => __( 'Translation Upgrade', 'woocommerce' ), - 'button' => __( 'Force Translation Upgrade', 'woocommerce' ), - 'desc' => __( 'Note: This option will force the translation upgrade for your language if a translation is available.', 'woocommerce' ), - ); - } - - return apply_filters( 'woocommerce_debug_tools', $tools ); + $tools_controller = new WC_REST_System_Status_Tools_Controller; + return $tools_controller->get_tools(); } /** - * Show the logs page + * Show the logs page. */ public static function status_logs() { + if ( defined( 'WC_LOG_HANDLER' ) && 'WC_Log_Handler_DB' === WC_LOG_HANDLER ) { + self::status_logs_db(); + } else { + self::status_logs_file(); + } + } + + /** + * Show the log page contents for file log handler. + */ + public static function status_logs_file() { + $logs = self::scan_log_files(); - if ( ! empty( $_POST['log_file'] ) && isset( $logs[ sanitize_title( $_POST['log_file'] ) ] ) ) { - $viewed_log = $logs[ sanitize_title( $_POST['log_file'] ) ]; - } elseif ( $logs ) { + + if ( ! empty( $_REQUEST['log_file'] ) && isset( $logs[ sanitize_title( $_REQUEST['log_file'] ) ] ) ) { + $viewed_log = $logs[ sanitize_title( $_REQUEST['log_file'] ) ]; + } elseif ( ! empty( $logs ) ) { $viewed_log = current( $logs ); } + + $handle = ! empty( $viewed_log ) ? self::get_log_file_handle( $viewed_log ) : ''; + + if ( ! empty( $_REQUEST['handle'] ) ) { + self::remove_log(); + } + include_once( 'views/html-admin-page-status-logs.php' ); } /** - * Retrieve metadata from a file. Based on WP Core's get_file_data function - * - * @since 2.1.1 - * @param string $file Path to the file - * @param array $all_headers List of headers, in the format array('HeaderKey' => 'Header Name') + * Show the log page contents for db log handler. + */ + public static function status_logs_db() { + + // Flush + if ( ! empty( $_REQUEST['flush-logs'] ) ) { + self::flush_db_logs(); + } + + // Bulk actions + if ( isset( $_GET['action'] ) && isset( $_GET['log'] ) ) { + self::log_table_bulk_actions(); + } + + $log_table_list = new WC_Admin_Log_Table_List(); + $log_table_list->prepare_items(); + + include_once( 'views/html-admin-page-status-logs-db.php' ); + } + + /** + * Retrieve metadata from a file. Based on WP Core's get_file_data function. + * @since 2.1.1 + * @param string $file Path to the file + * @return string */ public static function get_file_version( $file ) { + + // Avoid notices if file does not exist + if ( ! file_exists( $file ) ) { + return ''; + } + // We don't need to write to the file, so just open for reading. $fp = fopen( $file, 'r' ); @@ -256,24 +151,39 @@ class WC_Admin_Status { $file_data = str_replace( "\r", "\n", $file_data ); $version = ''; - if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( '@version', '/' ) . '(.*)$/mi', $file_data, $match ) && $match[1] ) + if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( '@version', '/' ) . '(.*)$/mi', $file_data, $match ) && $match[1] ) { $version = _cleanup_header_comment( $match[1] ); + } return $version ; } /** - * Scan the template files + * Return the log file handle. * - * @param string $template_path + * @param string $filename + * @return string + */ + public static function get_log_file_handle( $filename ) { + return substr( $filename, 0, strlen( $filename ) > 37 ? strlen( $filename ) - 37 : strlen( $filename ) - 4 ); + } + + /** + * Scan the template files. + * @param string $template_path * @return array */ public static function scan_template_files( $template_path ) { - $files = scandir( $template_path ); - $result = array(); - if ( $files ) { + + $files = @scandir( $template_path ); + $result = array(); + + if ( ! empty( $files ) ) { + foreach ( $files as $key => $value ) { - if ( ! in_array( $value, array( ".",".." ) ) ) { + + if ( ! in_array( $value, array( ".", ".." ) ) ) { + if ( is_dir( $template_path . DIRECTORY_SEPARATOR . $value ) ) { $sub_files = self::scan_template_files( $template_path . DIRECTORY_SEPARATOR . $value ); foreach ( $sub_files as $sub_file ) { @@ -289,15 +199,17 @@ class WC_Admin_Status { } /** - * Scan the log files - * + * Scan the log files. * @return array */ public static function scan_log_files() { - $files = @scandir( WC_LOG_DIR ); - $result = array(); - if ( $files ) { + $files = @scandir( WC_LOG_DIR ); + $result = array(); + + if ( ! empty( $files ) ) { + foreach ( $files as $key => $value ) { + if ( ! in_array( $value, array( '.', '..' ) ) ) { if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) { $result[ sanitize_title( $value ) ] = $value; @@ -305,6 +217,110 @@ class WC_Admin_Status { } } } + return $result; } + + /** + * Get latest version of a theme by slug. + * @param object $theme WP_Theme object. + * @return string Version number if found. + */ + public static function get_latest_theme_version( $theme ) { + include_once( ABSPATH . 'wp-admin/includes/theme.php' ); + + $api = themes_api( 'theme_information', array( + 'slug' => $theme->get_stylesheet(), + 'fields' => array( + 'sections' => false, + 'tags' => false, + ), + ) ); + + $update_theme_version = 0; + + // Check .org for updates. + if ( is_object( $api ) && ! is_wp_error( $api ) ) { + $update_theme_version = $api->version; + + // Check WooThemes Theme Version. + } elseif ( strstr( $theme->{'Author URI'}, 'woothemes' ) ) { + $theme_dir = substr( strtolower( str_replace( ' ','', $theme->Name ) ), 0, 45 ); + + if ( false === ( $theme_version_data = get_transient( $theme_dir . '_version_data' ) ) ) { + $theme_changelog = wp_safe_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $theme_dir . '/changelog.txt' ); + $cl_lines = explode( "\n", wp_remote_retrieve_body( $theme_changelog ) ); + if ( ! empty( $cl_lines ) ) { + foreach ( $cl_lines as $line_num => $cl_line ) { + if ( preg_match( '/^[0-9]/', $cl_line ) ) { + $theme_date = str_replace( '.' , '-' , trim( substr( $cl_line , 0 , strpos( $cl_line , '-' ) ) ) ); + $theme_version = preg_replace( '~[^0-9,.]~' , '' ,stristr( $cl_line , "version" ) ); + $theme_update = trim( str_replace( "*" , "" , $cl_lines[ $line_num + 1 ] ) ); + $theme_version_data = array( 'date' => $theme_date , 'version' => $theme_version , 'update' => $theme_update , 'changelog' => $theme_changelog ); + set_transient( $theme_dir . '_version_data', $theme_version_data , DAY_IN_SECONDS ); + break; + } + } + } + } + + if ( ! empty( $theme_version_data['version'] ) ) { + $update_theme_version = $theme_version_data['version']; + } + } + + return $update_theme_version; + } + + /** + * Remove/delete the chosen file. + */ + public static function remove_log() { + if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'remove_log' ) ) { + wp_die( __( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + if ( ! empty( $_REQUEST['handle'] ) ) { + $log_handler = new WC_Log_Handler_File(); + $log_handler->remove( $_REQUEST['handle'] ); + } + + wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); + exit(); + } + + /** + * Clear DB log table. + * + * @since 3.0.0 + */ + private static function flush_db_logs() { + if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { + wp_die( __( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + WC_Log_Handler_DB::flush(); + + wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); + exit(); + } + + /** + * Bulk DB log table actions. + * + * @since 3.0.0 + */ + private static function log_table_bulk_actions() { + if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { + wp_die( __( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + $log_ids = array_map( 'absint', (array) $_GET['log'] ); + + if ( 'delete' === $_GET['action'] || 'delete' === $_GET['action2'] ) { + WC_Log_Handler_DB::delete( $log_ids ); + wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); + exit(); + } + } } diff --git a/includes/admin/class-wc-admin-taxonomies.php b/includes/admin/class-wc-admin-taxonomies.php index 1676b2bac5e..6844bc7c6a3 100644 --- a/includes/admin/class-wc-admin-taxonomies.php +++ b/includes/admin/class-wc-admin-taxonomies.php @@ -1,31 +1,34 @@ attribute_name . '_pre_add_form', array( $this, 'product_attribute_description' ) ); + } + } // Maintain hierarchy of terms add_filter( 'wp_terms_checklist_args', array( $this, 'disable_checked_ontop' ) ); @@ -44,15 +54,14 @@ class WC_Admin_Taxonomies { /** * Order term when created (put in position 0). * - * @access public * @param mixed $term_id * @param mixed $tt_id - * @param mixed $taxonomy - * @return void + * @param string $taxonomy */ public function create_term( $term_id, $tt_id = '', $taxonomy = '' ) { - if ( $taxonomy != 'product_cat' && ! taxonomy_is_product_attribute( $taxonomy ) ) + if ( 'product_cat' != $taxonomy && ! taxonomy_is_product_attribute( $taxonomy ) ) { return; + } $meta_name = taxonomy_is_product_attribute( $taxonomy ) ? 'order_' . esc_attr( $taxonomy ) : 'order'; @@ -62,30 +71,24 @@ class WC_Admin_Taxonomies { /** * When a term is deleted, delete its meta. * - * @access public * @param mixed $term_id - * @return void */ public function delete_term( $term_id ) { - - $term_id = (int) $term_id; - - if ( ! $term_id ) - return; - global $wpdb; - $wpdb->query( "DELETE FROM {$wpdb->woocommerce_termmeta} WHERE `woocommerce_term_id` = " . $term_id ); + + $term_id = absint( $term_id ); + + if ( $term_id && get_option( 'db_version' ) < 34370 ) { + $wpdb->delete( $wpdb->woocommerce_termmeta, array( 'woocommerce_term_id' => $term_id ), array( '%d' ) ); + } } /** * Category thumbnail fields. - * - * @access public - * @return void */ public function add_category_fields() { ?> -
    +
    -
    +
    -
    -
    +
    +
    @@ -159,19 +182,18 @@ class WC_Admin_Taxonomies { /** * Edit category thumbnail field. * - * @access public * @param mixed $term Term (category) being edited - * @param mixed $taxonomy Taxonomy of the term being edited */ - public function edit_category_fields( $term, $taxonomy ) { + public function edit_category_fields( $term ) { - $display_type = get_woocommerce_term_meta( $term->term_id, 'display_type', true ); - $image = ''; - $thumbnail_id = absint( get_woocommerce_term_meta( $term->term_id, 'thumbnail_id', true ) ); - if ( $thumbnail_id ) + $display_type = get_woocommerce_term_meta( $term->term_id, 'display_type', true ); + $thumbnail_id = absint( get_woocommerce_term_meta( $term->term_id, 'thumbnail_id', true ) ); + + if ( $thumbnail_id ) { $image = wp_get_attachment_thumb_url( $thumbnail_id ); - else + } else { $image = wc_placeholder_img_src(); + } ?> @@ -187,18 +209,23 @@ class WC_Admin_Taxonomies { -
    -
    +
    +
    - - + +
    -

    - - - -
    - - intro(); ?> - - - -
    -

    -
    -
    -

    -

    Orders, Coupons, Customers, Products and Reports in both XML and JSON formats.', 'woocommerce' ); ?>

    -
    -
    -
    -

    -

    OAuth 1.0a specification if you don\'t have SSL. Data is only available to authenticated users.', 'woocommerce' ); ?>

    -
    -
    -
    -
    -

    -
    -
    -

    -

    -
    -
    -

    -

    -

    -
    -
    -

    -

    filtering capabilities, a new customer report showing orders/spending, and the ability to export CSVs.', 'woocommerce' ); ?>

    -

    -
    -
    -
    -
    -

    -
    -
    -

    -

    -
    -
    -

    -

    -

    -
    -
    -

    -

    -

    -
    -
    -

    -

    -

    -
    -
    -

    -

    -

    -
    -
    -

    -

    -

    -
    -
    -
    -
    -

    - -
    -
    -

    -

    -
    - -
    -

    -

    -
    - -
    -

    -

    -
    -
    -
    - -
    -

    -

    default_credit_card_form.', 'woocommerce' ); ?>

    -
    - -
    -

    -

    -
    - -
    -

    -

    -
    - -
    -
    - -
    -

    -

    -
    - -
    -

    -

    -
    - -
    -

    -

    -
    - -
    -
    - -
    - -
    -
    - -
    - - intro(); ?> - -

    Contribute to WooCommerce.', 'woocommerce' ); ?>

    - - contributors(); ?> -
    - - - get_contributors(); - - if ( empty( $contributors ) ) - return ''; - - $contributor_list = ''; - - return $contributor_list; - } - - /** - * Retrieve list of contributors from GitHub. - * - * @access public - * @return mixed - */ - public function get_contributors() { - $contributors = get_transient( 'woocommerce_contributors' ); - - if ( false !== $contributors ) - return $contributors; - - $response = wp_remote_get( 'https://api.github.com/repos/woothemes/woocommerce/contributors', array( 'sslverify' => false ) ); - - if ( is_wp_error( $response ) || 200 != wp_remote_retrieve_response_code( $response ) ) - return array(); - - $contributors = json_decode( wp_remote_retrieve_body( $response ) ); - - if ( ! is_array( $contributors ) ) - return array(); - - set_transient( 'woocommerce_contributors', $contributors, 3600 ); - - return $contributors; - } - - /** - * Sends user to the welcome page on first activation - */ - public function welcome() { - - // Bail if no activation redirect transient is set - if ( ! get_transient( '_wc_activation_redirect' ) ) - return; - - // Delete the redirect transient - delete_transient( '_wc_activation_redirect' ); - - // Bail if we are waiting to install or update via the interface update/install links - if ( get_option( '_wc_needs_update' ) == 1 || get_option( '_wc_needs_pages' ) == 1 ) - return; - - // Bail if activating from network, or bulk, or within an iFrame - if ( is_network_admin() || isset( $_GET['activate-multi'] ) || defined( 'IFRAME_REQUEST' ) ) - return; - - if ( ( isset( $_GET['action'] ) && 'upgrade-plugin' == $_GET['action'] ) && ( isset( $_GET['plugin'] ) && strstr( $_GET['plugin'], 'woocommerce.php' ) ) ) - return; - - wp_redirect( admin_url( 'index.php?page=wc-about' ) ); - exit; - } -} - -new WC_Admin_Welcome(); diff --git a/includes/admin/class-wc-admin.php b/includes/admin/class-wc-admin.php index 82e0c97ffb3..1ca5210d75a 100644 --- a/includes/admin/class-wc-admin.php +++ b/includes/admin/class-wc-admin.php @@ -1,66 +1,97 @@ id ) { case 'dashboard' : @@ -79,47 +110,141 @@ class WC_Admin { } /** - * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from accessing admin + * Handle redirects to setup/welcome page after install and updates. + * + * For setup wizard, transient must be present, the user must have access rights, and we must ignore the network/bulk plugin updaters. + */ + public function admin_redirects() { + // Nonced plugin install redirects (whitelisted) + if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) { + $plugin_slug = wc_clean( $_GET['wc-install-plugin-redirect'] ); + + if ( current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ) ) ) { + $nonce = wp_create_nonce( 'install-plugin_' . $plugin_slug ); + $url = self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug . '&_wpnonce=' . $nonce ); + } else { + $url = admin_url( 'plugin-install.php?tab=search&type=term&s=' . $plugin_slug ); + } + + wp_safe_redirect( $url ); + exit; + } + + // Setup wizard redirect + if ( get_transient( '_wc_activation_redirect' ) ) { + delete_transient( '_wc_activation_redirect' ); + + if ( ( ! empty( $_GET['page'] ) && in_array( $_GET['page'], array( 'wc-setup' ) ) ) || is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_woocommerce' ) || apply_filters( 'woocommerce_prevent_automatic_wizard_redirect', false ) ) { + return; + } + + // If the user needs to install, send them to the setup wizard + if ( WC_Admin_Notices::has_notice( 'install' ) ) { + wp_safe_redirect( admin_url( 'index.php?page=wc-setup' ) ); + exit; + } + } + } + + /** + * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from accessing admin. */ public function prevent_admin_access() { $prevent_access = false; - if ( 'yes' == get_option( 'woocommerce_lock_down_admin' ) && ! is_ajax() && ! ( current_user_can( 'edit_posts' ) || current_user_can( 'manage_woocommerce' ) ) && basename( $_SERVER["SCRIPT_FILENAME"] ) !== 'admin-post.php' ) { - $prevent_access = true; + if ( 'yes' === get_option( 'woocommerce_lock_down_admin', 'yes' ) && ! is_ajax() && basename( $_SERVER["SCRIPT_FILENAME"] ) !== 'admin-post.php' ) { + $has_cap = false; + $access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' ); + + foreach ( $access_caps as $access_cap ) { + if ( current_user_can( $access_cap ) ) { + $has_cap = true; + break; + } + } + + if ( ! $has_cap ) { + $prevent_access = true; + } } - $prevent_access = apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ); - - if ( $prevent_access ) { - wp_safe_redirect( get_permalink( wc_get_page_id( 'myaccount' ) ) ); + if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) { + wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit; } } /** - * Preview email template - * @return [type] + * Preview email template. + * + * @return string */ public function preview_emails() { + if ( isset( $_GET['preview_woocommerce_mail'] ) ) { - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'preview-mail') ) { + if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'preview-mail' ) ) { die( 'Security check' ); } - global $email_heading; - - ob_start(); - - include( 'views/html-email-template-preview.php' ); - + // load the mailer class $mailer = WC()->mailer(); - $message = ob_get_clean(); - $email_heading = __( 'HTML Email Template', 'woocommerce' ); - echo $mailer->wrap_message( $email_heading, $message ); + // get the preview email subject + $email_heading = __( 'HTML email template', 'woocommerce' ); + + // get the preview email content + ob_start(); + include( 'views/html-email-template-preview.php' ); + $message = ob_get_clean(); + + // create a new email + $email = new WC_Email(); + + // wrap the content with the email template and then add styles + $message = apply_filters( 'woocommerce_mail_content', $email->style_inline( $mailer->wrap_message( $email_heading, $message ) ) ); + + // print the preview email + echo $message; exit; } } + + /** + * Change the admin footer text on WooCommerce admin pages. + * + * @since 2.3 + * @param string $footer_text + * @return string + */ + public function admin_footer_text( $footer_text ) { + if ( ! current_user_can( 'manage_woocommerce' ) || ! function_exists( 'wc_get_screen_ids' ) ) { + return $footer_text; + } + $current_screen = get_current_screen(); + $wc_pages = wc_get_screen_ids(); + + // Set only WC pages. + $wc_pages = array_diff( $wc_pages, array( 'profile', 'user-edit' ) ); + + // Check to make sure we're on a WooCommerce admin page. + if ( isset( $current_screen->id ) && apply_filters( 'woocommerce_display_admin_footer_text', in_array( $current_screen->id, $wc_pages ) ) ) { + // Change the footer text + if ( ! get_option( 'woocommerce_admin_footer_text_rated' ) ) { + /* translators: %s: five stars */ + $footer_text = sprintf( __( 'If you like WooCommerce please leave us a %s rating. A huge thanks in advance!', 'woocommerce' ), '★★★★★' ); + wc_enqueue_js( " + jQuery( 'a.wc-rating-link' ).click( function() { + jQuery.post( '" . WC()->ajax_url() . "', { action: 'woocommerce_rated' } ); + jQuery( this ).parent().text( jQuery( this ).data( 'rated' ) ); + }); + " ); + } else { + $footer_text = __( 'Thank you for selling with WooCommerce.', 'woocommerce' ); + } + } + + return $footer_text; + } } return new WC_Admin(); diff --git a/includes/admin/helper/class-wc-helper-api.php b/includes/admin/helper/class-wc-helper-api.php new file mode 100644 index 00000000000..f24775c2cc3 --- /dev/null +++ b/includes/admin/helper/class-wc-helper-api.php @@ -0,0 +1,130 @@ + parse_url( $url, PHP_URL_HOST ), + 'request_uri' => $request_uri, + 'method' => ! empty( $args['method'] ) ? $args['method'] : 'GET', + ); + + if ( ! empty( $args['body'] ) ) { + $data['body'] = $args['body']; + } + + $signature = hash_hmac( 'sha256', json_encode( $data ), $auth['access_token_secret'] ); + if ( empty( $args['headers'] ) ) { + $args['headers'] = array(); + } + + $args['headers'] = array( + 'Authorization' => 'Bearer ' . $auth['access_token'], + 'X-Woo-Signature' => $signature, + ); + } + + /** + * Wrapper for self::request(). + * + * @param string $endpoint The helper API endpoint to request. + * @param array $args Arguments passed to wp_remote_request(). + * + * @return array The response object from wp_safe_remote_request(). + */ + public static function get( $endpoint, $args = array() ) { + $args['method'] = 'GET'; + return self::request( $endpoint, $args ); + } + + /** + * Wrapper for self::request(). + * + * @param string $endpoint The helper API endpoint to request. + * @param array $args Arguments passed to wp_remote_request(). + * + * @return array The response object from wp_safe_remote_request(). + */ + public static function post( $endpoint, $args = array() ) { + $args['method'] = 'POST'; + return self::request( $endpoint, $args ); + } + + /** + * Using the API base, form a request URL from a given endpoint. + * + * @param string $endpoint The endpoint to request. + * + * @return string The absolute endpoint URL. + */ + public static function url( $endpoint ) { + $endpoint = ltrim( $endpoint, '/' ); + $endpoint = sprintf( '%s/%s', self::$api_base, $endpoint ); + $endpoint = esc_url_raw( $endpoint ); + return $endpoint; + } +} + +WC_Helper_API::load(); diff --git a/includes/admin/helper/class-wc-helper-compat.php b/includes/admin/helper/class-wc-helper-compat.php new file mode 100644 index 00000000000..1a1eecc7b7c --- /dev/null +++ b/includes/admin/helper/class-wc-helper-compat.php @@ -0,0 +1,172 @@ +admin, 'maybe_display_activation_notice' ) ); + remove_action( 'admin_notices', array( $GLOBALS['woothemes_updater']->admin, 'maybe_display_activation_notice' ) ); + remove_action( 'network_admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) ); + remove_action( 'admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) ); + } + + /** + * Attempt to migrate a legacy connection to a new one. + */ + public static function migrate_connection() { + // Don't attempt to migrate if attempted before. + if ( WC_Helper_Options::get( 'did-migrate' ) ) { + return; + } + + $auth = WC_Helper_Options::get( 'auth' ); + if ( ! empty( $auth ) ) { + return; + } + + WC_Helper::log( 'Attempting oauth/migrate' ); + WC_Helper_Options::update( 'did-migrate', true ); + + $master_key = get_option( 'woothemes_helper_master_key' ); + if ( empty( $master_key ) ) { + WC_Helper::log( 'Master key not found, aborting' ); + return; + } + + $request = WC_Helper_API::post( 'oauth/migrate', array( + 'body' => array( + 'home_url' => home_url(), + 'master_key' => $master_key, + ), + ) ); + + if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) { + WC_Helper::log( 'Call to oauth/migrate returned a non-200 response code' ); + return; + } + + $request_token = json_decode( wp_remote_retrieve_body( $request ) ); + if ( empty( $request_token ) ) { + WC_Helper::log( 'Call to oauth/migrate returned an empty token' ); + return; + } + + // Obtain an access token. + $request = WC_Helper_API::post( 'oauth/access_token', array( + 'body' => array( + 'request_token' => $request_token, + 'home_url' => home_url(), + 'migrate' => true, + ), + ) ); + + if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) { + WC_Helper::log( 'Call to oauth/access_token returned a non-200 response code' ); + return; + } + + $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( empty( $access_token ) ) { + WC_Helper::log( 'Call to oauth/access_token returned an invalid token' ); + return; + } + + WC_Helper_Options::update( 'auth', array( + 'access_token' => $access_token['access_token'], + 'access_token_secret' => $access_token['access_token_secret'], + 'site_id' => $access_token['site_id'], + 'user_id' => null, // Set this later + 'updated' => time(), + ) ); + + // Obtain the connected user info. + if ( ! WC_Helper::_flush_authentication_cache() ) { + WC_Helper::log( 'Could not obtain connected user info in migrate_connection' ); + WC_Helper_Options::update( 'auth', array() ); + return; + } + } + + /** + * Attempt to deactivate the legacy helper plugin. + */ + public static function deactivate_plugin() { + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + if ( ! function_exists( 'deactivate_plugins' ) ) { + return; + } + + if ( is_plugin_active( 'woothemes-updater/woothemes-updater.php' ) ) { + deactivate_plugins( 'woothemes-updater/woothemes-updater.php' ); + } + } + + /** + * Register menu item. + */ + public static function admin_menu() { + // No additional menu items for users who did not have a connected helper before. + $master_key = get_option( 'woothemes_helper_master_key' ); + if ( empty( $master_key ) ) { + return; + } + + // Do not show the menu item if user has already seen the new screen. + $auth = WC_Helper_Options::get( 'auth' ); + if ( ! empty( $auth['user_id'] ) ) { + return; + } + + add_dashboard_page( __( 'WooCommerce Helper', 'woocommerce' ), __( 'WooCommerce Helper', 'woocommerce' ), 'manage_options', 'woothemes-helper', array( __CLASS__, 'render_compat_menu' ) ); + } + + /** + * Render the legacy helper compat view. + */ + public static function render_compat_menu() { + $helper_url = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + ), admin_url( 'admin.php' ) ); + include( WC_Helper::get_view_filename( 'html-helper-compat.php' ) ); + } +} + +WC_Helper_Compat::load(); diff --git a/includes/admin/helper/class-wc-helper-options.php b/includes/admin/helper/class-wc-helper-options.php new file mode 100644 index 00000000000..b11612f4fd5 --- /dev/null +++ b/includes/admin/helper/class-wc-helper-options.php @@ -0,0 +1,54 @@ +slug ) ) { + return $response; + } + + $found_plugin = null; + + // Look through local Woo plugins by slugs. + foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) { + $slug = dirname( $plugin['_filename'] ); + if ( dirname( $plugin['_filename'] ) === $args->slug ) { + $plugin['_slug'] = $args->slug; + $found_plugin = $plugin; + break; + } + } + + if ( ! $found_plugin ) { + return $response; + } + + // Fetch the product information from the Helper API. + $request = WC_Helper_API::get( add_query_arg( array( + 'product_id' => absint( $plugin['_product_id'] ), + 'product_slug' => rawurlencode( $plugin['_slug'] ), + ), 'info' ), array( 'authenticated' => true ) ); + + $results = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( ! empty( $results ) ) { + $response = (object) $results; + } + + return $response; + } +} + +WC_Helper_Plugin_Info::load(); diff --git a/includes/admin/helper/class-wc-helper-updater.php b/includes/admin/helper/class-wc-helper-updater.php new file mode 100644 index 00000000000..9a3024dd679 --- /dev/null +++ b/includes/admin/helper/class-wc-helper-updater.php @@ -0,0 +1,240 @@ + 'woo-' . $plugin['_product_id'], + 'slug' => $data['slug'], + 'plugin' => $filename, + 'new_version' => $data['version'], + 'url' => $data['url'], + 'package' => '', + 'upgrade_notice' => $data['upgrade_notice'], + ); + + if ( self::_has_active_subscription( $plugin['_product_id'] ) ) { + $item['package'] = $data['package']; + } + + if ( version_compare( $plugin['Version'], $data['version'], '<' ) ) { + $transient->response[ $filename ] = (object) $item; + unset( $transient->no_update[ $filename ] ); + } else { + $transient->no_update[ $filename ] = (object) $item; + unset( $transient->response[ $filename ] ); + } + } + + return $transient; + } + + /** + * Runs on pre_set_site_transient_update_themes, provides custom + * packages for WooCommerce.com-hosted extensions. + * + * @param object $transient The update_themes transient object. + * + * @return object The same or a modified version of the transient. + */ + public static function transient_update_themes( $transient ) { + $update_data = self::get_update_data(); + + foreach ( WC_Helper::get_local_woo_themes() as $theme ) { + if ( empty( $update_data[ $theme['_product_id'] ] ) ) { + continue; + } + + $data = $update_data[ $theme['_product_id'] ]; + $slug = $theme['_stylesheet']; + + $item = array( + 'theme' => $slug, + 'new_version' => $data['version'], + 'url' => $data['url'], + 'package' => '', + ); + + if ( self::_has_active_subscription( $theme['_product_id'] ) ) { + $item['package'] = $data['package']; + } + + if ( version_compare( $theme['Version'], $data['version'], '<' ) ) { + $transient->response[ $slug ] = $item; + } else { + unset( $transient->response[ $slug ] ); + $transient->checked[ $slug ] = $data['version']; + } + } + + return $transient; + } + + /** + * Get update data for all extensions. + * + * Scans through all subscriptions for the connected user, as well + * as all Woo extensions without a subscription, and obtains update + * data for each product. + * + * @return array Update data {product_id => data} + */ + public static function get_update_data() { + $payload = array(); + + // Scan subscriptions. + foreach ( WC_Helper::get_subscriptions() as $subscription ) { + $payload[ $subscription['product_id'] ] = array( + 'product_id' => $subscription['product_id'], + 'file_id' => '', + ); + } + + // Scan local plugins which may or may not have a subscription. + foreach ( WC_Helper::get_local_woo_plugins() as $data ) { + if ( ! isset( $payload[ $data['_product_id'] ] ) ) { + $payload[ $data['_product_id'] ] = array( + 'product_id' => $data['_product_id'], + ); + } + + $payload[ $data['_product_id'] ]['file_id'] = $data['_file_id']; + } + + // Scan local themes + foreach ( WC_Helper::get_local_woo_themes() as $data ) { + if ( ! isset( $payload[ $data['_product_id'] ] ) ) { + $payload[ $data['_product_id'] ] = array( + 'product_id' => $data['_product_id'], + ); + } + + $payload[ $data['_product_id'] ]['file_id'] = $data['_file_id']; + } + + return self::_update_check( $payload ); + } + + /** + * Run an update check API call. + * + * The call is cached based on the payload (product ids, file ids). If + * the payload changes, the cache is going to miss. + * + * @return array Update data for each requested product. + */ + private static function _update_check( $payload ) { + ksort( $payload ); + $hash = md5( json_encode( $payload ) ); + + $cache_key = '_woocommerce_helper_updates'; + if ( false !== ( $data = get_transient( $cache_key ) ) ) { + if ( hash_equals( $hash, $data['hash'] ) ) { + return $data['products']; + } + } + + $data = array( + 'hash' => $hash, + 'updated' => time(), + 'products' => array(), + 'errors' => array(), + ); + + $request = WC_Helper_API::post( 'update-check', array( + 'body' => json_encode( array( 'products' => $payload ) ), + 'authenticated' => true, + ) ); + + if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { + $data['errors'][] = 'http-error'; + } else { + $data['products'] = json_decode( wp_remote_retrieve_body( $request ), true ); + } + + set_transient( $cache_key, $data, 12 * HOUR_IN_SECONDS ); + return $data['products']; + } + + /** + * Check for an active subscription. + * + * Checks a given product id against all subscriptions on + * the current site. Returns true if at least one active + * subscription is found. + * + * @param int $product_id The product id to look for. + * + * @return bool True if active subscription found. + */ + private static function _has_active_subscription( $product_id ) { + if ( ! isset( $auth ) ) { + $auth = WC_Helper_Options::get( 'auth' ); + } + + if ( ! isset( $subscriptions ) ) { + $subscriptions = WC_Helper::get_subscriptions(); + } + + if ( empty( $auth['site_id'] ) || empty( $subscriptions ) ) { + return false; + } + + // Check for an active subscription. + foreach ( $subscriptions as $subscription ) { + if ( $subscription['product_id'] != $product_id ) { + continue; + } + + if ( in_array( absint( $auth['site_id'] ), $subscription['connections'] ) ) { + return true; + } + } + + return false; + } + + /** + * Flushes cached update data. + */ + public static function flush_updates_cache() { + delete_transient( '_woocommerce_helper_updates' ); + } +} + +WC_Helper_Updater::load(); diff --git a/includes/admin/helper/class-wc-helper.php b/includes/admin/helper/class-wc-helper.php new file mode 100644 index 00000000000..865baa669ad --- /dev/null +++ b/includes/admin/helper/class-wc-helper.php @@ -0,0 +1,1255 @@ + 'wc-addons', + 'section' => 'helper', + 'wc-helper-connect' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'connect' ), + ), admin_url( 'admin.php' ) ); + + include( self::get_view_filename( 'html-oauth-start.php' ) ); + return; + } + $disconnect_url = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-disconnect' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'disconnect' ), + ), admin_url( 'admin.php' ) ); + + $refresh_url = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-refresh' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'refresh' ), + ), admin_url( 'admin.php' ) ); + + // Installed plugins and themes, with or without an active subscription. + $woo_plugins = self::get_local_woo_plugins(); + $woo_themes = self::get_local_woo_themes(); + + $site_id = absint( $auth['site_id'] ); + $subscriptions = self::get_subscriptions(); + $updates = WC_Helper_Updater::get_update_data(); + $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' ); + + foreach ( $subscriptions as &$subscription ) { + $subscription['active'] = in_array( $site_id, $subscription['connections'] ); + + $subscription['activate_url'] = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-activate' => 1, + 'wc-helper-product-key' => $subscription['product_key'], + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'activate:' . $subscription['product_key'] ), + ), admin_url( 'admin.php' ) ); + + $subscription['deactivate_url'] = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-deactivate' => 1, + 'wc-helper-product-key' => $subscription['product_key'], + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'deactivate:' . $subscription['product_key'] ), + ), admin_url( 'admin.php' ) ); + + $subscription['local'] = array( + 'installed' => false, + 'active' => false, + 'version' => null, + ); + + $subscription['update_url'] = admin_url( 'update-core.php' ); + + $local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) ); + + if ( ! empty( $local ) ) { + $local = array_shift( $local ); + $subscription['local']['installed'] = true; + $subscription['local']['version'] = $local['Version']; + + if ( 'plugin' == $local['_type'] ) { + if ( is_plugin_active( $local['_filename'] ) ) { + $subscription['local']['active'] = true; + } elseif ( is_multisite() && is_plugin_active_for_network( $local['_filename'] ) ) { + $subscription['local']['active'] = true; + } + + // A magic update_url. + $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] ); + + } elseif ( 'theme' == $local['_type'] ) { + if ( in_array( $local['_stylesheet'], array( get_stylesheet(), get_template() ) ) ) { + $subscription['local']['active'] = true; + } + + // Another magic update_url. + $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] ); + } + } + + $subscription['has_update'] = false; + if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { + $subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' ); + } + + $subscription['download_primary'] = true; + $subscription['download_url'] = $subscription['product_url']; + if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { + $subscription['download_url'] = $updates[ $subscription['product_id'] ]['package']; + } + + $subscription['actions'] = array(); + + if ( $subscription['has_update'] && ! $subscription['expired'] ) { + $action = array( + /* translators: %s: version number */ + 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), + 'button_label' => __( 'Update', 'woocommerce' ), + 'button_url' => $subscription['update_url'], + 'status' => 'update-available', + 'icon' => 'dashicons-update', + ); + + // Subscription is not active on this site. + if ( ! $subscription['active'] ) { + $action['message'] .= ' ' . __( 'To enable this update you need to activate this subscription.', 'woocommerce' ); + $action['button_label'] = null; + $action['button_url'] = null; + } + + $subscription['actions'][] = $action; + } + + if ( $subscription['has_update'] && $subscription['expired'] ) { + $action = array( + /* translators: %s: version number */ + 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $action['message'] .= ' ' . __( 'To enable this update you need to purchase a new subscription.', 'woocommerce' ); + $action['button_label'] = __( 'Purchase', 'woocommerce' ); + $action['button_url'] = $subscription['product_url']; + + $subscription['actions'][] = $action; + } elseif ( $subscription['expired'] && ! empty( $subscription['master_user_email'] ) ) { + $action = array( + 'message' => sprintf( __( 'This subscription has expired. Contact the owner to renew the subscription to receive updates and support.', 'woocommerce' ) ), + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['actions'][] = $action; + } elseif ( $subscription['expired'] ) { + $action = array( + 'message' => sprintf( __( 'This subscription has expired. Please renew to receive updates and support.', 'woocommerce' ) ), + 'button_label' => __( 'Renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['actions'][] = $action; + } + + if ( $subscription['expiring'] && ! $subscription['autorenew'] ) { + $action = array( + 'message' => __( 'Subscription is expiring soon.', 'woocommerce' ), + 'button_label' => __( 'Enable auto-renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['download_primary'] = false; + $subscription['actions'][] = $action; + } elseif ( $subscription['expiring'] ) { + $action = array( + 'message' => sprintf( __( 'This subscription is expiring soon. Please renew to continue receiving updates and support.', 'woocommerce' ) ), + 'button_label' => __( 'Renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['download_primary'] = false; + $subscription['actions'][] = $action; + } + + // Mark the first action primary. + foreach ( $subscription['actions'] as $key => $action ) { + if ( ! empty( $action['button_label'] ) ) { + $subscription['actions'][ $key ]['primary'] = true; + break; + } + } + } + + // Break the by-ref. + unset( $subscription ); + + // Installed products without a subscription. + $no_subscriptions = array(); + foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) { + if ( in_array( $data['_product_id'], $subscriptions_product_ids ) ) { + continue; + } + + $data['_product_url'] = '#'; + $data['_has_update'] = false; + + if ( ! empty( $updates[ $data['_product_id'] ] ) ) { + $data['_has_update'] = version_compare( $updates[ $data['_product_id'] ]['version'], $data['Version'], '>' ); + + if ( ! empty( $updates[ $data['_product_id'] ]['url'] ) ) { + $data['_product_url'] = $updates[ $data['_product_id'] ]['url']; + } elseif ( ! empty( $data['PluginURI'] ) ) { + $data['_product_url'] = $data['PluginURI']; + } + } + + $data['_actions'] = array(); + + if ( $data['_has_update'] ) { + $action = array( + 'message' => sprintf( __( 'Version %s is available. To enable this update you need to purchase a new subscription.', 'woocommerce' ), esc_html( $updates[ $data['_product_id'] ]['version'] ) ), + 'button_label' => __( 'Purchase', 'woocommerce' ), + 'button_url' => $data['_product_url'], + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $data['_actions'][] = $action; + } else { + $action = array( + 'message' => sprintf( __( 'To receive updates and support for this extension, you need to purchase a new subscription or be added as a collaborator.', 'woocommerce' ), 'https://docs.woocommerce.com/document/adding-collaborators/' ), + 'button_label' => __( 'Purchase', 'woocommerce' ), + 'button_url' => $data['_product_url'], + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $data['_actions'][] = $action; + } + + $no_subscriptions[ $filename ] = $data; + } + + // Update the user id if it came from a migrated connection. + if ( empty( $auth['user_id'] ) ) { + $auth['user_id'] = get_current_user_id(); + WC_Helper_Options::update( 'auth', $auth ); + } + + // Sort alphabetically + uasort( $subscriptions, array( __CLASS__, '_sort_by_product_name' ) ); + uasort( $no_subscriptions, array( __CLASS__, '_sort_by_name' ) ); + + // We have an active connection. + include( self::get_view_filename( 'html-main.php' ) ); + return; + } + + /** + * Enqueue admin scripts and styles. + */ + public static function admin_enqueue_scripts() { + $screen = get_current_screen(); + + if ( 'woocommerce_page_wc-addons' == $screen->id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { + wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), WC_VERSION ); + } + } + + /** + * Various success/error notices. + * + * Runs during admin page render, so no headers/redirects here. + * + * @return array Array pairs of message/type strings with notices. + */ + private static function _get_return_notices() { + $return_status = isset( $_GET['wc-helper-status'] ) ? $_GET['wc-helper-status'] : null; + $notices = array(); + + switch ( $return_status ) { + case 'activate-success': + $subscription = self::_get_subscriptions_from_product_id( absint( $_GET['wc-helper-product-id'] ) ); + $notices[] = array( + 'type' => 'updated', + /* translators: %s: product name */ + 'message' => sprintf( __( '%s activated successfully. You will now receive updates for this product.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' ), + ); + break; + + case 'activate-error': + $subscription = self::_get_subscriptions_from_product_id( absint( $_GET['wc-helper-product-id'] ) ); + $notices[] = array( + 'type' => 'error', + /* translators: %s: product name */ + 'message' => sprintf( __( 'An error has occurred when activating %s. Please try again later.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' ), + ); + break; + + case 'deactivate-success': + $subscription = self::_get_subscriptions_from_product_id( absint( $_GET['wc-helper-product-id'] ) ); + $local = self::_get_local_from_product_id( absint( $_GET['wc-helper-product-id'] ) ); + + /* translators: %s: product name */ + $message = sprintf( __( 'Subscription for %s deactivated successfully. You will no longer receive updates for this product.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' ); + + if ( $local && is_plugin_active( $local['_filename'] ) && current_user_can( 'activate_plugins' ) ) { + $deactivate_plugin_url = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-deactivate-plugin' => 1, + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'deactivate-plugin:' . $subscription['product_id'] ), + ), admin_url( 'admin.php' ) ); + + /* translators: %1$s: product name, %2$s: deactivate url */ + $message = sprintf( __( 'Subscription for %1$s deactivated successfully. You will no longer receive updates for this product. Click here if you wish to deactive the plugin as well.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '', esc_url( $deactivate_plugin_url ) ); + } + + $notices[] = array( + 'message' => $message, + 'type' => 'updated', + ); + break; + + case 'deactivate-error': + $subscription = self::_get_subscriptions_from_product_id( absint( $_GET['wc-helper-product-id'] ) ); + $notices[] = array( + 'type' => 'error', + /* translators: %s: product name */ + 'message' => sprintf( __( 'An error has occurred when deactivating the subscription for %s. Please try again later.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' ), + ); + break; + + case 'deactivate-plugin-success': + $subscription = self::_get_subscriptions_from_product_id( absint( $_GET['wc-helper-product-id'] ) ); + $notices[] = array( + 'type' => 'updated', + /* translators: %s: product name */ + 'message' => sprintf( __( 'The extension %s has been deactivated successfully.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' ), + ); + break; + + case 'deactivate-plugin-error': + $subscription = self::_get_subscriptions_from_product_id( absint( $_GET['wc-helper-product-id'] ) ); + $notices[] = array( + 'type' => 'error', + /* translators: %1$s: product name, %2$s: plugins screen url */ + 'message' => sprintf( __( 'An error has occurred when deactivating the extension %1$s. Please proceed to the Plugins screen to deactivate it manually.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '', admin_url( 'plugins.php' ) ), + ); + break; + + case 'helper-connected': + $notices[] = array( + 'message' => __( 'You have successfully connected your store to WooCommerce.com', 'woocommerce' ), + 'type' => 'updated', + ); + break; + + case 'helper-disconnected': + $notices[] = array( + 'message' => __( 'You have successfully disconnected your store from WooCommerce.com', 'woocommerce' ), + 'type' => 'updated', + ); + break; + + case 'helper-refreshed': + $notices[] = array( + 'message' => __( 'Authentication and subscription caches refreshed successfully.', 'woocommerce' ), + 'type' => 'updated', + ); + break; + } + + return $notices; + } + + /** + * Various early-phase actions with possible redirects. + */ + public static function current_screen( $screen ) { + if ( 'woocommerce_page_wc-addons' != $screen->id ) { + return; + } + + if ( empty( $_GET['section'] ) || 'helper' !== $_GET['section'] ) { + return; + } + + if ( ! empty( $_GET['wc-helper-connect'] ) ) { + return self::_helper_auth_connect(); + } + + if ( ! empty( $_GET['wc-helper-return'] ) ) { + return self::_helper_auth_return(); + } + + if ( ! empty( $_GET['wc-helper-disconnect'] ) ) { + return self::_helper_auth_disconnect(); + } + + if ( ! empty( $_GET['wc-helper-refresh'] ) ) { + return self::_helper_auth_refresh(); + } + + if ( ! empty( $_GET['wc-helper-activate'] ) ) { + return self::_helper_subscription_activate(); + } + + if ( ! empty( $_GET['wc-helper-deactivate'] ) ) { + return self::_helper_subscription_deactivate(); + } + + if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) { + return self::_helper_plugin_deactivate(); + } + } + + /** + * Initiate a new OAuth connection. + */ + private static function _helper_auth_connect() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( $_GET['wc-helper-nonce'], 'connect' ) ) { + self::log( 'Could not verify nonce in _helper_auth_connect' ); + wp_die( 'Could not verify nonce' ); + } + + $redirect_uri = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-return' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'connect' ), + ), admin_url( 'admin.php' ) ); + + $request = WC_Helper_API::post( 'oauth/request_token', array( + 'body' => array( + 'home_url' => home_url(), + 'redirect_uri' => $redirect_uri, + ), + ) ); + + $code = wp_remote_retrieve_response_code( $request ); + + if ( 200 !== $code ) { + self::log( sprintf( 'Call to oauth/request_token returned a non-200 response code (%d)', $code ) ); + wp_die( 'Something went wrong' ); + } + + $secret = json_decode( wp_remote_retrieve_body( $request ) ); + if ( empty( $secret ) ) { + self::log( sprintf( 'Call to oauth/request_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); + wp_die( 'Something went wrong' ); + } + + $connect_url = add_query_arg( array( + 'home_url' => rawurlencode( home_url() ), + 'redirect_uri' => rawurlencode( $redirect_uri ), + 'secret' => rawurlencode( $secret ), + ), WC_Helper_API::url( 'oauth/authorize' ) ); + + wp_redirect( esc_url_raw( $connect_url ) ); + die(); + } + + /** + * Return from WooCommerce.com OAuth flow. + */ + private static function _helper_auth_return() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( $_GET['wc-helper-nonce'], 'connect' ) ) { + self::log( 'Could not verify nonce in _helper_auth_return' ); + wp_die( 'Something went wrong' ); + } + + // Bail if the user clicked deny. + if ( ! empty( $_GET['deny'] ) ) { + wp_safe_redirect( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ); + die(); + } + + // We do need a request token... + if ( empty( $_GET['request_token'] ) ) { + self::log( 'Request token not found in _helper_auth_return' ); + wp_die( 'Something went wrong' ); + } + + // Obtain an access token. + $request = WC_Helper_API::post( 'oauth/access_token', array( + 'body' => array( + 'request_token' => $_GET['request_token'], + 'home_url' => home_url(), + ), + ) ); + + $code = wp_remote_retrieve_response_code( $request ); + + if ( 200 !== $code ) { + self::log( sprintf( 'Call to oauth/access_token returned a non-200 response code (%d)', $code ) ); + wp_die( 'Something went wrong' ); + } + + $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( ! $access_token ) { + self::log( sprintf( 'Call to oauth/access_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); + wp_die( 'Something went wrong' ); + } + + WC_Helper_Options::update( 'auth', array( + 'access_token' => $access_token['access_token'], + 'access_token_secret' => $access_token['access_token_secret'], + 'site_id' => $access_token['site_id'], + 'user_id' => get_current_user_id(), + 'updated' => time(), + ) ); + + // Obtain the connected user info. + if ( ! self::_flush_authentication_cache() ) { + self::log( 'Could not obtain connected user info in _helper_auth_return' ); + WC_Helper_Options::update( 'auth', array() ); + wp_die( 'Something went wrong.' ); + } + + self::_flush_subscriptions_cache(); + + // Enable tracking when connected. + if ( class_exists( 'WC_Tracker' ) ) { + update_option( 'woocommerce_allow_tracking', 'yes' ); + WC_Tracker::send_tracking_data( true ); + } + + wp_safe_redirect( add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => 'helper-connected', + ), admin_url( 'admin.php' ) ) ); + die(); + } + + /** + * Disconnect from WooCommerce.com, clear OAuth tokens. + */ + private static function _helper_auth_disconnect() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( $_GET['wc-helper-nonce'], 'disconnect' ) ) { + self::log( 'Could not verify nonce in _helper_auth_disconnect' ); + wp_die( 'Could not verify nonce' ); + } + + $redirect_uri = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => 'helper-disconnected', + ), admin_url( 'admin.php' ) ); + + $result = WC_Helper_API::post( 'oauth/invalidate_token', array( + 'authenticated' => true, + ) ); + + WC_Helper_Options::update( 'auth', array() ); + WC_Helper_Options::update( 'auth_user_data', array() ); + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + // Disable tracking when disconnected. + update_option( 'woocommerce_allow_tracking', 'no' ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * User hit the Refresh button, clear all caches. + */ + private static function _helper_auth_refresh() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( $_GET['wc-helper-nonce'], 'refresh' ) ) { + self::log( 'Could not verify nonce in _helper_auth_refresh' ); + wp_die( 'Could not verify nonce' ); + } + + $redirect_uri = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => 'helper-refreshed', + ), admin_url( 'admin.php' ) ); + + self::_flush_authentication_cache(); + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Active a product subscription. + */ + private static function _helper_subscription_activate() { + $product_key = $_GET['wc-helper-product-key']; + $product_id = absint( $_GET['wc-helper-product-id'] ); + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( $_GET['wc-helper-nonce'], 'activate:' . $product_key ) ) { + self::log( 'Could not verify nonce in _helper_subscription_activate' ); + wp_die( 'Could not verify nonce' ); + } + + $request = WC_Helper_API::post( 'activate', array( + 'authenticated' => true, + 'body' => json_encode( array( + 'product_key' => $product_key, + ) ), + ) ); + + $activated = wp_remote_retrieve_response_code( $request ) === 200; + $body = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' == $body['code'] ) { + $activated = true; + } + + // Attempt to activate this plugin. + $local = self::_get_local_from_product_id( $product_id ); + if ( $local && 'plugin' == $local['_type'] && current_user_can( 'activate_plugins' ) && ! is_plugin_active( $local['_filename'] ) ) { + activate_plugin( $local['_filename'] ); + } + + self::_flush_subscriptions_cache(); + $redirect_uri = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => $activated ? 'activate-success' : 'activate-error', + 'wc-helper-product-id' => $product_id, + ), admin_url( 'admin.php' ) ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Deactivate a product subscription. + */ + private static function _helper_subscription_deactivate() { + $product_key = $_GET['wc-helper-product-key']; + $product_id = absint( $_GET['wc-helper-product-id'] ); + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( $_GET['wc-helper-nonce'], 'deactivate:' . $product_key ) ) { + self::log( 'Could not verify nonce in _helper_subscription_deactivate' ); + wp_die( 'Could not verify nonce' ); + } + + $request = WC_Helper_API::post( 'deactivate', array( + 'authenticated' => true, + 'body' => json_encode( array( + 'product_key' => $product_key, + ) ), + ) ); + + $code = wp_remote_retrieve_response_code( $request ); + $deactivated = 200 === $code; + if ( ! $deactivated ) { + self::log( sprintf( 'Deactivate API call returned a non-200 response code (%d)', $code ) ); + } + + self::_flush_subscriptions_cache(); + $redirect_uri = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error', + 'wc-helper-product-id' => $product_id, + ), admin_url( 'admin.php' ) ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Deactivate a plugin. + */ + private static function _helper_plugin_deactivate() { + $product_id = absint( $_GET['wc-helper-product-id'] ); + $deactivated = false; + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( $_GET['wc-helper-nonce'], 'deactivate-plugin:' . $product_id ) ) { + self::log( 'Could not verify nonce in _helper_plugin_deactivate' ); + wp_die( 'Could not verify nonce' ); + } + + if ( ! current_user_can( 'activate_plugins' ) ) { + wp_die( 'You are not allowed to manage plugins on this site.' ); + } + + $local = wp_list_filter( array_merge( self::get_local_woo_plugins(), + self::get_local_woo_themes() ), array( '_product_id' => $product_id ) ); + + // Attempt to deactivate this plugin or theme. + if ( ! empty( $local ) ) { + $local = array_shift( $local ); + if ( is_plugin_active( $local['_filename'] ) ) { + deactivate_plugins( $local['_filename'] ); + } + + $deactivated = ! is_plugin_active( $local['_filename'] ); + } + + $redirect_uri = add_query_arg( array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => $deactivated ? 'deactivate-plugin-success' : 'deactivate-plugin-error', + 'wc-helper-product-id' => $product_id, + ), admin_url( 'admin.php' ) ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Get a local plugin/theme entry from product_id. + * + * @param int $product_id The product id. + * + * @return array|bool The array containing the local plugin/theme data or false. + */ + private static function _get_local_from_product_id( $product_id ) { + $local = wp_list_filter( array_merge( self::get_local_woo_plugins(), + self::get_local_woo_themes() ), array( '_product_id' => $product_id ) ); + + if ( ! empty( $local ) ) { + return array_shift( $local ); + } + + return false; + } + + /** + * Get a subscription entry from product_id. If multiple subscriptions are + * found with the same product id and $single is set to true, will return the + * first one in the list, so you can use this method to get things like extension + * name, version, etc. + * + * @param int $product_id The product id. + * @param bool $single Whether to return a single subscription or all matching a product id. + * + * @return array|bool The array containing sub data or false. + */ + private static function _get_subscriptions_from_product_id( $product_id, $single = true ) { + $subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_id' => $product_id ) ); + if ( ! empty( $subscriptions ) ) { + return $single ? array_shift( $subscriptions ) : $subscriptions; + } + + return false; + } + + /** + * Additional theme style.css and plugin file headers. + * + * Format: Woo: product_id:file_id + */ + public static function extra_headers( $headers ) { + $headers[] = 'Woo'; + return $headers; + } + + /** + * Obtain a list of locally installed Woo extensions. + */ + public static function get_local_woo_plugins() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + $plugins = get_plugins(); + $woo_plugins = array(); + + // Back-compat for woothemes_queue_update(). + $_compat = array(); + if ( ! empty( $GLOBALS['woothemes_queued_updates'] ) ) { + foreach ( $GLOBALS['woothemes_queued_updates'] as $_compat_plugin ) { + $_compat[ $_compat_plugin->file ] = array( + 'product_id' => $_compat_plugin->product_id, + 'file_id' => $_compat_plugin->file_id, + ); + } + } + + foreach ( $plugins as $filename => $data ) { + if ( empty( $data['Woo'] ) && ! empty( $_compat[ $filename ] ) ) { + $data['Woo'] = sprintf( '%d:%s', $_compat[ $filename ]['product_id'], $_compat[ $filename ]['file_id'] ); + } + + if ( empty( $data['Woo'] ) ) { + continue; + } + + list( $product_id, $file_id ) = explode( ':', $data['Woo'] ); + if ( empty( $product_id ) || empty( $file_id ) ) { + continue; + } + + $data['_filename'] = $filename; + $data['_product_id'] = absint( $product_id ); + $data['_file_id'] = $file_id; + $data['_type'] = 'plugin'; + $woo_plugins[ $filename ] = $data; + } + + return $woo_plugins; + } + + /** + * Get locally installed Woo themes. + */ + public static function get_local_woo_themes() { + $themes = wp_get_themes(); + $woo_themes = array(); + + foreach ( $themes as $theme ) { + $header = $theme->get( 'Woo' ); + + // Back-compat for theme_info.txt + if ( ! $header ) { + $txt = $theme->get_stylesheet_directory() . '/theme_info.txt'; + if ( is_readable( $txt ) ) { + $txt = file_get_contents( $txt ); + $txt = preg_split( "#\s#", $txt ); + if ( count( $txt ) >= 2 ) { + $header = sprintf( '%d:%s', $txt[0], $txt[1] ); + } + } + } + + if ( empty( $header ) ) { + continue; + } + + list( $product_id, $file_id ) = explode( ':', $header ); + if ( empty( $product_id ) || empty( $file_id ) ) { + continue; + } + + $data = array( + 'Name' => $theme->get( 'Name' ), + 'Version' => $theme->get( 'Version' ), + 'Woo' => $header, + + '_filename' => $theme->get_stylesheet() . '/style.css', + '_stylesheet' => $theme->get_stylesheet(), + '_product_id' => absint( $product_id ), + '_file_id' => $file_id, + '_type' => 'theme', + ); + + $woo_themes[ $data['_filename'] ] = $data; + } + + return $woo_themes; + } + + /** + * Get the connected user's subscriptions. + * + * @return array + */ + public static function get_subscriptions() { + $cache_key = '_woocommerce_helper_subscriptions'; + if ( false !== ( $data = get_transient( $cache_key ) ) ) { + return $data; + } + + // Obtain the connected user info. + $request = WC_Helper_API::get( 'subscriptions', array( + 'authenticated' => true, + ) ); + + if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { + set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS ); + return array(); + } + + $data = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( empty( $data ) || ! is_array( $data ) ) { + $data = array(); + } + + set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS ); + return $data; + } + + /** + * Runs when any plugin is activated. + * + * Depending on the activated plugin attempts to look through available + * subscriptions and auto-activate one if possible, so the user does not + * need to visit the Helper UI at all after installing a new extension. + * + * @param string $filename The filename of the activated plugin. + */ + public static function activated_plugin( $filename ) { + $plugins = self::get_local_woo_plugins(); + + // Not a local woo plugin + if ( empty( $plugins[ $filename ] ) ) { + return; + } + + // Make sure we have a connection. + $auth = WC_Helper_Options::get( 'auth' ); + if ( empty( $auth ) ) { + return; + } + + $plugin = $plugins[ $filename ]; + $subscriptions = self::_get_subscriptions_from_product_id( $plugin['_product_id'], false ); + + // No valid subscriptions for this product + if ( empty( $subscriptions ) ) { + return; + } + + $subscription = null; + foreach ( $subscriptions as $_sub ) { + + // Don't attempt to activate expired subscriptions. + if ( $_sub['expired'] ) { + continue; + } + + // No more sites available in this subscription. + if ( $_sub['sites_active'] >= $_sub['sites_max'] ) { + continue; + } + + // Looks good. + $subscription = $_sub; + break; + } + + // No valid subscription found. + if ( ! $subscription ) { + return; + } + + $request = WC_Helper_API::post( 'activate', array( + 'authenticated' => true, + 'body' => json_encode( array( + 'product_key' => $subscription['product_key'], + ) ), + ) ); + + $activated = wp_remote_retrieve_response_code( $request ) === 200; + $body = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' == $body['code'] ) { + $activated = true; + } + + if ( ! $activated ) { + self::log( 'Could not activate a subscription upon plugin activation: ' . $$filename ); + return; + } + + self::log( 'Auto-activated a subscripton for ' . $filename ); + self::_flush_subscriptions_cache(); + } + + /** + * Runs when any plugin is deactivated. + * + * When a user deactivates a plugin, attempt to deactivate any subscriptions + * associated with the extension. + * + * @param string $filename The filename of the deactivated plugin. + */ + public static function deactivated_plugin( $filename ) { + $plugins = self::get_local_woo_plugins(); + + // Not a local woo plugin + if ( empty( $plugins[ $filename ] ) ) { + return; + } + + // Make sure we have a connection. + $auth = WC_Helper_Options::get( 'auth' ); + if ( empty( $auth ) ) { + return; + } + + $plugin = $plugins[ $filename ]; + $subscriptions = self::_get_subscriptions_from_product_id( $plugin['_product_id'], false ); + $site_id = absint( $auth['site_id'] ); + + // No valid subscriptions for this product + if ( empty( $subscriptions ) ) { + return; + } + + $deactivated = 0; + + foreach ( $subscriptions as $subscription ) { + // Don't touch subscriptions that aren't activated on this site. + if ( ! in_array( $site_id, $subscription['connections'] ) ) { + continue; + } + + $request = WC_Helper_API::post( 'deactivate', array( + 'authenticated' => true, + 'body' => json_encode( array( + 'product_key' => $subscription['product_key'], + ) ), + ) ); + + if ( wp_remote_retrieve_response_code( $request ) === 200 ) { + $deactivated++; + } + } + + if ( $deactivated ) { + self::log( sprintf( 'Auto-deactivated %d subscripton(s) for %s', $deactivated, $filename ) ); + self::_flush_subscriptions_cache(); + } + } + + /** + * Add a note about available extension updates if Woo core has an update available. + */ + public static function admin_notices() { + $screen = get_current_screen(); + if ( 'update-core' !== $screen->id ) { + return; + } + + // Don't nag if Woo doesn't have an update available. + if ( ! self::_woo_core_update_available() ) { + return; + } + + $notice = self::_get_extensions_update_notice(); + if ( ! empty( $notice ) ) { + echo '

    ' . $notice . '

    '; + } + } + + /** + * Add an upgrade notice if there are extensions with updates. + * + * @param string $message An existing update notice or an empty string. + * + * @return string The resulting message. + */ + public static function in_plugin_update_message( $message ) { + $notice = self::_get_extensions_update_notice(); + if ( ! empty( $notice ) ) { + $message .= '

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

    +

    +

    View and manage your extensions now.', 'woocommerce' ), esc_url( $helper_url ) ); ?>

    +
    diff --git a/includes/admin/helper/views/html-main.php b/includes/admin/helper/views/html-main.php new file mode 100644 index 00000000000..473275455b0 --- /dev/null +++ b/includes/admin/helper/views/html-main.php @@ -0,0 +1,177 @@ + + +
    + +

    + + + +
    +

    + +

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

    + +

    +
    + + + +
    + + +

    +

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

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

    + +

    +
    + +
    + +
    diff --git a/includes/admin/helper/views/html-oauth-start.php b/includes/admin/helper/views/html-oauth-start.php new file mode 100644 index 00000000000..5c5657dbd2c --- /dev/null +++ b/includes/admin/helper/views/html-oauth-start.php @@ -0,0 +1,21 @@ + + +
    + +

    + + +
    +
    + WooCommerce + + +

    Sorry to see you go. Feel free to reconnect again using the button below.', 'woocommerce' ); ?>

    + + +

    +

    +

    +
    +
    +
    diff --git a/includes/admin/helper/views/html-section-account.php b/includes/admin/helper/views/html-section-account.php new file mode 100644 index 00000000000..72f2c60df5c --- /dev/null +++ b/includes/admin/helper/views/html-section-account.php @@ -0,0 +1,15 @@ + + + + diff --git a/includes/admin/helper/views/html-section-nav.php b/includes/admin/helper/views/html-section-nav.php new file mode 100644 index 00000000000..0e8cb9d2605 --- /dev/null +++ b/includes/admin/helper/views/html-section-nav.php @@ -0,0 +1,6 @@ + + + diff --git a/includes/admin/helper/views/html-section-notices.php b/includes/admin/helper/views/html-section-notices.php new file mode 100644 index 00000000000..46792dab880 --- /dev/null +++ b/includes/admin/helper/views/html-section-notices.php @@ -0,0 +1,7 @@ + + + +
    + +
    + diff --git a/includes/admin/importers/class-wc-product-csv-importer-controller.php b/includes/admin/importers/class-wc-product-csv-importer-controller.php new file mode 100644 index 00000000000..bd9505c98a2 --- /dev/null +++ b/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -0,0 +1,562 @@ +steps = array( + 'upload' => array( + 'name' => __( 'Upload CSV file', 'woocommerce' ), + 'view' => array( $this, 'upload_form' ), + 'handler' => array( $this, 'upload_form_handler' ), + ), + 'mapping' => array( + 'name' => __( 'Column mapping', 'woocommerce' ), + 'view' => array( $this, 'mapping_form' ), + 'handler' => '', + ), + 'import' => array( + 'name' => __( 'Import', 'woocommerce' ), + 'view' => array( $this, 'import' ), + 'handler' => '', + ), + 'done' => array( + 'name' => __( 'Done!', 'woocommerce' ), + 'view' => array( $this, 'done' ), + 'handler' => '', + ), + ); + $this->step = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) ); + $this->file = isset( $_REQUEST['file'] ) ? wc_clean( $_REQUEST['file'] ) : ''; + $this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false; + $this->delimiter = ! empty( $_REQUEST['delimiter'] ) ? wc_clean( $_REQUEST['delimiter'] ) : ','; + } + + /** + * Get the URL for the next step's screen. + * @param string step slug (default: current step) + * @return string URL for next step if a next step exists. + * Admin URL if it's the last step. + * Empty string on failure. + */ + public function get_next_step_link( $step = '' ) { + if ( ! $step ) { + $step = $this->step; + } + + $keys = array_keys( $this->steps ); + + if ( end( $keys ) === $step ) { + return admin_url(); + } + + $step_index = array_search( $step, $keys ); + + if ( false === $step_index ) { + return ''; + } + + $params = array( + 'step' => $keys[ $step_index + 1 ], + 'file' => str_replace( DIRECTORY_SEPARATOR, '/', $this->file ), + 'delimiter' => $this->delimiter, + 'update_existing' => $this->update_existing, + '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ), // wp_nonce_url() escapes & to & breaking redirects. + ); + + return add_query_arg( $params ); + } + + /** + * Output header view. + */ + protected function output_header() { + include( dirname( __FILE__ ) . '/views/html-csv-import-header.php' ); + } + + /** + * Output steps view. + */ + protected function output_steps() { + include( dirname( __FILE__ ) . '/views/html-csv-import-steps.php' ); + } + + /** + * Output footer view. + */ + protected function output_footer() { + include( dirname( __FILE__ ) . '/views/html-csv-import-footer.php' ); + } + + /** + * Add error message. + */ + protected function add_error( $error ) { + $this->errors[] = $error; + } + + /** + * Add error message. + */ + protected function output_errors() { + if ( $this->errors ) { + foreach ( $this->errors as $error ) { + echo '

    ' . esc_html( $error ) . '

    '; + } + } + } + + /** + * Dispatch current step and show correct view. + */ + public function dispatch() { + if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) { + call_user_func( $this->steps[ $this->step ]['handler'], $this ); + } + $this->output_header(); + $this->output_steps(); + $this->output_errors(); + call_user_func( $this->steps[ $this->step ]['view'], $this ); + $this->output_footer(); + } + + /** + * Output information about the uploading process. + */ + protected function upload_form() { + $bytes = apply_filters( 'import_upload_size_limit', wp_max_upload_size() ); + $size = size_format( $bytes ); + $upload_dir = wp_upload_dir(); + + include( dirname( __FILE__ ) . '/views/html-product-csv-import-form.php' ); + } + + /** + * Handle the upload form and store options. + */ + public function upload_form_handler() { + check_admin_referer( 'woocommerce-csv-importer' ); + + $file = $this->handle_upload(); + + if ( is_wp_error( $file ) ) { + $this->add_error( $file->get_error_message() ); + return; + } else { + $this->file = $file; + } + + wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); + exit; + } + + /** + * Handles the CSV upload and initial parsing of the file to prepare for + * displaying author import options. + * + * @return string|WP_Error + */ + public function handle_upload() { + $valid_filetypes = apply_filters( 'woocommerce_csv_product_import_valid_filetypes', array( 'csv' => 'text/csv', 'txt' => 'text/plain' ) ); + + if ( empty( $_POST['file_url'] ) ) { + if ( ! isset( $_FILES['import'] ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); + } + + $filetype = wp_check_filetype( $_FILES['import']['name'], $valid_filetypes ); + if ( ! in_array( $filetype['type'], $valid_filetypes ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + } + + $overrides = array( 'test_form' => false, 'mimes' => $valid_filetypes ); + $upload = wp_handle_upload( $_FILES['import'], $overrides ); + + if ( isset( $upload['error'] ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_upload_error', $upload['error'] ); + } + + // Construct the object array. + $object = array( + 'post_title' => basename( $upload['file'] ), + 'post_content' => $upload['url'], + 'post_mime_type' => $upload['type'], + 'guid' => $upload['url'], + 'context' => 'import', + 'post_status' => 'private', + ); + + // Save the data. + $id = wp_insert_attachment( $object, $upload['file'] ); + + /* + * Schedule a cleanup for one day from now in case of failed + * import or missing wp_import_cleanup() call. + */ + wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) ); + + return $upload['file']; + } elseif ( file_exists( ABSPATH . $_POST['file_url'] ) ) { + $filetype = wp_check_filetype( ABSPATH . $_POST['file_url'], $valid_filetypes ); + if ( ! in_array( $filetype['type'], $valid_filetypes ) ) { + return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + } + + return ABSPATH . $_POST['file_url']; + } + + return new WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce' ) ); + } + + /** + * Mapping step. + */ + protected function mapping_form() { + $args = array( + 'lines' => 1, + 'delimiter' => $this->delimiter, + ); + + $importer = self::get_importer( $this->file, $args ); + $headers = $importer->get_raw_keys(); + $mapped_items = $this->auto_map_columns( $headers ); + $sample = current( $importer->get_raw_data() ); + + if ( empty( $sample ) ) { + $this->add_error( __( 'The file is empty, please try again with a new file.', 'woocommerce' ) ); + return; + } + + include_once( dirname( __FILE__ ) . '/views/html-csv-import-mapping.php' ); + } + + /** + * Import the file if it exists and is valid. + */ + public function import() { + if ( ! is_file( $this->file ) ) { + $this->add_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); + return; + } + + if ( ! empty( $_POST['map_to'] ) ) { + $mapping = wp_unslash( $_POST['map_to'] ); + } else { + wp_redirect( esc_url_raw( $this->get_next_step_link( 'upload' ) ) ); + exit; + } + + wp_localize_script( 'wc-product-import', 'wc_product_import_params', array( + 'import_nonce' => wp_create_nonce( 'wc-product-import' ), + 'mapping' => $mapping, + 'file' => $this->file, + 'update_existing' => $this->update_existing, + 'delimiter' => $this->delimiter, + ) ); + wp_enqueue_script( 'wc-product-import' ); + + include_once( dirname( __FILE__ ) . '/views/html-csv-import-progress.php' ); + } + + /** + * Done step. + */ + protected function done() { + $imported = isset( $_GET['products-imported'] ) ? absint( $_GET['products-imported'] ) : 0; + $updated = isset( $_GET['products-updated'] ) ? absint( $_GET['products-updated'] ) : 0; + $failed = isset( $_GET['products-failed'] ) ? absint( $_GET['products-failed'] ) : 0; + $skipped = isset( $_GET['products-skipped'] ) ? absint( $_GET['products-skipped'] ) : 0; + $errors = array_filter( (array) get_user_option( 'product_import_error_log' ) ); + + include_once( dirname( __FILE__ ) . '/views/html-csv-import-done.php' ); + } + + /** + * Auto map column names. + * + * @param array $raw_headers Raw header columns. + * @param bool $num_indexes If should use numbers or raw header columns as indexes. + * @return array + */ + protected function auto_map_columns( $raw_headers, $num_indexes = true ) { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + + include( dirname( __FILE__ ) . '/mappings/mappings.php' ); + + /** + * @hooked wc_importer_generic_mappings - 10 + * @hooked wc_importer_wordpress_mappings - 10 + */ + $default_columns = apply_filters( 'woocommerce_csv_product_import_mapping_default_columns', array( + __( 'ID', 'woocommerce' ) => 'id', + __( 'Type', 'woocommerce' ) => 'type', + __( 'SKU', 'woocommerce' ) => 'sku', + __( 'Name', 'woocommerce' ) => 'name', + __( 'Published', 'woocommerce' ) => 'published', + __( 'Is featured?', 'woocommerce' ) => 'featured', + __( 'Visibility in catalog', 'woocommerce' ) => 'catalog_visibility', + __( 'Short description', 'woocommerce' ) => 'short_description', + __( 'Description', 'woocommerce' ) => 'description', + __( 'Date sale price starts', 'woocommerce' ) => 'date_on_sale_from', + __( 'Date sale price ends', 'woocommerce' ) => 'date_on_sale_to', + __( 'Tax status', 'woocommerce' ) => 'tax_status', + __( 'Tax class', 'woocommerce' ) => 'tax_class', + __( 'In stock?', 'woocommerce' ) => 'stock_status', + __( 'Stock', 'woocommerce' ) => 'stock_quantity', + __( 'Backorders allowed?', 'woocommerce' ) => 'backorders', + __( 'Sold individually?', 'woocommerce' ) => 'sold_individually', + sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ) => 'weight', + sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ) => 'length', + sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ) => 'width', + sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ) => 'height', + __( 'Allow customer reviews?', 'woocommerce' ) => 'reviews_allowed', + __( 'Purchase note', 'woocommerce' ) => 'purchase_note', + __( 'Sale price', 'woocommerce' ) => 'sale_price', + __( 'Regular price', 'woocommerce' ) => 'regular_price', + __( 'Categories', 'woocommerce' ) => 'category_ids', + __( 'Tags', 'woocommerce' ) => 'tag_ids', + __( 'Shipping class', 'woocommerce' ) => 'shipping_class_id', + __( 'Images', 'woocommerce' ) => 'images', + __( 'Download limit', 'woocommerce' ) => 'download_limit', + __( 'Download expiry days', 'woocommerce' ) => 'download_expiry', + __( 'Parent', 'woocommerce' ) => 'parent_id', + __( 'Upsells', 'woocommerce' ) => 'upsell_ids', + __( 'Cross-sells', 'woocommerce' ) => 'cross_sell_ids', + __( 'Grouped products', 'woocommerce' ) => 'grouped_products', + __( 'External URL', 'woocommerce' ) => 'product_url', + __( 'Button text', 'woocommerce' ) => 'button_text', + ) ); + + $special_columns = $this->get_special_columns( apply_filters( 'woocommerce_csv_product_import_mapping_special_columns', + array( + __( 'Attribute %d name', 'woocommerce' ) => 'attributes:name', + __( 'Attribute %d value(s)', 'woocommerce' ) => 'attributes:value', + __( 'Attribute %d visible', 'woocommerce' ) => 'attributes:visible', + __( 'Attribute %d global', 'woocommerce' ) => 'attributes:taxonomy', + __( 'Attribute %d default', 'woocommerce' ) => 'attributes:default', + __( 'Download %d name', 'woocommerce' ) => 'downloads:name', + __( 'Download %d URL', 'woocommerce' ) => 'downloads:url', + __( 'Meta: %s', 'woocommerce' ) => 'meta:', + ) + ) ); + + $headers = array(); + foreach ( $raw_headers as $key => $field ) { + $index = $num_indexes ? $key : $field; + $headers[ $index ] = $field; + + if ( isset( $default_columns[ $field ] ) ) { + $headers[ $index ] = $default_columns[ $field ]; + } else { + foreach ( $special_columns as $regex => $special_key ) { + if ( preg_match( $regex, $field, $matches ) ) { + $headers[ $index ] = $special_key . $matches[1]; + break; + } + } + } + } + + return apply_filters( 'woocommerce_csv_product_import_mapped_columns', $headers, $raw_headers ); + } + + /** + * Sanitize special column name regex. + * + * @param string $value Raw special column name. + * @return string + */ + protected function sanitize_special_column_name_regex( $value ) { + return '/' . str_replace( array( '%d', '%s' ), '(.*)', quotemeta( $value ) ) . '/'; + } + + /** + * Get special columns. + * + * @param array $columns Raw special columns. + * @return array + */ + protected function get_special_columns( $columns ) { + $formatted = array(); + + foreach ( $columns as $key => $value ) { + $regex = $this->sanitize_special_column_name_regex( $key ); + + $formatted[ $regex ] = $value; + } + + return $formatted; + } + + /** + * Get mapping options. + * + * @param string $item Item name + * @return array + */ + protected function get_mapping_options( $item = '' ) { + // Get index for special column names. + $index = $item; + + if ( preg_match( '/\d+$/', $item, $matches ) ) { + $index = $matches[0]; + } + + // Properly format for meta field. + $meta = str_replace( 'meta:', '', $item ); + + // Available options. + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $options = array( + 'id' => __( 'ID', 'woocommerce' ), + 'type' => __( 'Type', 'woocommerce' ), + 'sku' => __( 'SKU', 'woocommerce' ), + 'name' => __( 'Name', 'woocommerce' ), + 'published' => __( 'Published', 'woocommerce' ), + 'featured' => __( 'Is featured?', 'woocommerce' ), + 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ), + 'short_description' => __( 'Short description', 'woocommerce' ), + 'description' => __( 'Description', 'woocommerce' ), + 'price' => array( + 'name' => __( 'Price', 'woocommerce' ), + 'options' => array( + 'regular_price' => __( 'Regular price', 'woocommerce' ), + 'sale_price' => __( 'Sale price', 'woocommerce' ), + 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ), + 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ), + ), + ), + 'tax_status' => __( 'Tax status', 'woocommerce' ), + 'tax_class' => __( 'Tax class', 'woocommerce' ), + 'stock_status' => __( 'In stock?', 'woocommerce' ), + 'stock_quantity' => _x( 'Stock', 'Quantity in stock', 'woocommerce' ), + 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), + 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), + /* translators: %s: weight unit */ + 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ), + 'dimensions' => array( + 'name' => __( 'Dimensions', 'woocommerce' ), + 'options' => array( + /* translators: %s: dimension unit */ + 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ), + /* translators: %s: dimension unit */ + 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ), + /* translators: %s: dimension unit */ + 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ), + ), + ), + 'category_ids' => __( 'Categories', 'woocommerce' ), + 'tag_ids' => __( 'Tags', 'woocommerce' ), + 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ), + 'images' => __( 'Images', 'woocommerce' ), + 'parent_id' => __( 'Parent', 'woocommerce' ), + 'upsell_ids' => __( 'Upsells', 'woocommerce' ), + 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ), + 'grouped_products' => __( 'Grouped products', 'woocommerce' ), + 'external' => array( + 'name' => __( 'External product', 'woocommerce' ), + 'options' => array( + 'product_url' => __( 'External URL', 'woocommerce' ), + 'button_text' => __( 'Button text', 'woocommerce' ), + ), + ), + 'downloads' => array( + 'name' => __( 'Downloads', 'woocommerce' ), + 'options' => array( + 'downloads:name' . $index => __( 'Download name', 'woocommerce' ), + 'downloads:url' . $index => __( 'Download URL', 'woocommerce' ), + 'download_limit' => __( 'Download limit', 'woocommerce' ), + 'download_expiry' => __( 'Download expiry days', 'woocommerce' ), + ), + ), + 'attributes' => array( + 'name' => __( 'Attributes', 'woocommerce' ), + 'options' => array( + 'attributes:name' . $index => __( 'Attribute name', 'woocommerce' ), + 'attributes:value' . $index => __( 'Attribute value(s)', 'woocommerce' ), + 'attributes:taxonomy' . $index => __( 'Is a global attribute?', 'woocommerce' ), + 'attributes:visible' . $index => __( 'Attribute visibility', 'woocommerce' ), + 'attributes:default' . $index => __( 'Default attribute', 'woocommerce' ), + ), + ), + 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), + 'purchase_note' => __( 'Purchase note', 'woocommerce' ), + 'meta:' . $meta => __( 'Import as meta', 'woocommerce' ), + ); + + return apply_filters( 'woocommerce_csv_product_import_mapping_options', $options, $item ); + } +} diff --git a/includes/admin/importers/class-wc-tax-rate-importer.php b/includes/admin/importers/class-wc-tax-rate-importer.php index f8e22abbb40..61fdacbc704 100644 --- a/includes/admin/importers/class-wc-tax-rate-importer.php +++ b/includes/admin/importers/class-wc-tax-rate-importer.php @@ -1,346 +1,314 @@ import_page = 'woocommerce_tax_rate_csv'; - } + /** + * The current delimiter. + * + * @var string + */ + public $delimiter; - /** - * Registered callback function for the WordPress Importer - * - * Manages the three separate stages of the CSV import process - */ - function dispatch() { - $this->header(); + /** + * Constructor. + */ + public function __construct() { + $this->import_page = 'woocommerce_tax_rate_csv'; + $this->delimiter = empty( $_POST['delimiter'] ) ? ',' : (string) wc_clean( $_POST['delimiter'] ); + } - if ( ! empty( $_POST['delimiter'] ) ) - $this->delimiter = stripslashes( trim( $_POST['delimiter'] ) ); + /** + * Registered callback function for the WordPress Importer. + * + * Manages the three separate stages of the CSV import process. + */ + public function dispatch() { - if ( ! $this->delimiter ) - $this->delimiter = ','; + $this->header(); - $step = empty( $_GET['step'] ) ? 0 : (int) $_GET['step']; - switch ( $step ) { - case 0: - $this->greet(); - break; - case 1: - check_admin_referer( 'import-upload' ); - if ( $this->handle_upload() ) { + $step = empty( $_GET['step'] ) ? 0 : (int) $_GET['step']; - if ( $this->id ) - $file = get_attached_file( $this->id ); - else - $file = ABSPATH . $this->file_url; + switch ( $step ) { - add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) ); + case 0: + $this->greet(); + break; - if ( function_exists( 'gc_enable' ) ) - gc_enable(); + case 1: + check_admin_referer( 'import-upload' ); - @set_time_limit(0); - @ob_flush(); - @flush(); + if ( $this->handle_upload() ) { - $this->import( $file ); + if ( $this->id ) { + $file = get_attached_file( $this->id ); + } else { + $file = ABSPATH . $this->file_url; } - break; - } - $this->footer(); - } - /** - * format_data_from_csv function. - * - * @access public - * @param mixed $data - * @param string $enc - * @return string - */ - function format_data_from_csv( $data, $enc ) { - return ( $enc == 'UTF-8' ) ? $data : utf8_encode( $data ); - } - - /** - * import function. - * - * @access public - * @param mixed $file - * @return void - */ - function import( $file ) { - global $wpdb; - - $this->imported = $this->skipped = 0; - - if ( ! is_file($file) ) { - echo '

    ' . __( 'Sorry, there has been an error.', 'woocommerce' ) . '
    '; - echo __( 'The file does not exist, please try again.', 'woocommerce' ) . '

    '; - $this->footer(); - die(); - } - - $new_rates = array(); - - ini_set( 'auto_detect_line_endings', '1' ); - - if ( ( $handle = fopen( $file, "r" ) ) !== FALSE ) { - - $header = fgetcsv( $handle, 0, $this->delimiter ); - - if ( sizeof( $header ) == 10 ) { - - $loop = 0; - - while ( ( $row = fgetcsv( $handle, 0, $this->delimiter ) ) !== FALSE ) { - - list( $country, $state, $postcode, $city, $rate, $name, $priority, $compound, $shipping, $class ) = $row; - - $country = trim( strtoupper( $country ) ); - $state = trim( strtoupper( $state ) ); - - if ( $country == '*' ) - $country = ''; - if ( $state == '*' ) - $state = ''; - if ( $class == 'standard' ) - $class = ''; - - $wpdb->insert( - $wpdb->prefix . "woocommerce_tax_rates", - array( - 'tax_rate_country' => $country, - 'tax_rate_state' => $state, - 'tax_rate' => wc_format_decimal( $rate, 4 ), - 'tax_rate_name' => trim( $name ), - 'tax_rate_priority' => absint( $priority ), - 'tax_rate_compound' => $compound ? 1 : 0, - 'tax_rate_shipping' => $shipping ? 1 : 0, - 'tax_rate_order' => $loop, - 'tax_rate_class' => sanitize_title( $class ) - ) - ); - - $tax_rate_id = $wpdb->insert_id; - - $postcode = wc_clean( $postcode ); - $postcodes = explode( ';', $postcode ); - $postcodes = array_map( 'strtoupper', array_map( 'wc_clean', $postcodes ) ); - foreach( $postcodes as $postcode ) { - if ( ! empty( $postcode ) && $postcode != '*' ) { - $wpdb->insert( - $wpdb->prefix . "woocommerce_tax_rate_locations", - array( - 'location_code' => $postcode, - 'tax_rate_id' => $tax_rate_id, - 'location_type' => 'postcode', - ) - ); - } - } - - $city = wc_clean( $city ); - $cities = explode( ';', $city ); - $cities = array_map( 'strtoupper', array_map( 'wc_clean', $cities ) ); - foreach( $cities as $city ) { - if ( ! empty( $city ) && $city != '*' ) { - $wpdb->insert( - $wpdb->prefix . "woocommerce_tax_rate_locations", - array( - 'location_code' => $city, - 'tax_rate_id' => $tax_rate_id, - 'location_type' => 'city', - ) - ); - } - } - - $loop ++; - $this->imported++; - } - - } else { - - echo '

    ' . __( 'Sorry, there has been an error.', 'woocommerce' ) . '
    '; - echo __( 'The CSV is invalid.', 'woocommerce' ) . '

    '; - $this->footer(); - die(); + add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) ); + $this->import( $file ); } - - fclose( $handle ); - } - - // Show Result - echo '

    - '.sprintf( __( 'Import complete - imported %s tax rates and skipped %s.', 'woocommerce' ), $this->imported, $this->skipped ).' -

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

    ' . __( 'All done!', 'woocommerce' ) . ' ' . __( 'View Tax Rates', 'woocommerce' ) . '' . '

    '; + $this->footer(); + } - do_action( 'import_end' ); + /** + * Import is starting. + */ + private function import_start() { + if ( function_exists( 'gc_enable' ) ) { + gc_enable(); + } + wc_set_time_limit( 0 ); + @ob_flush(); + @flush(); + @ini_set( 'auto_detect_line_endings', '1' ); + } + + /** + * UTF-8 encode the data if `$enc` value isn't UTF-8. + * + * @param mixed $data + * @param string $enc + * @return string + */ + public function format_data_from_csv( $data, $enc ) { + return ( 'UTF-8' === $enc ) ? $data : utf8_encode( $data ); + } + + /** + * Import the file if it exists and is valid. + * + * @param mixed $file + */ + public function import( $file ) { + if ( ! is_file( $file ) ) { + $this->import_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); } - /** - * Handles the CSV upload and initial parsing of the file to prepare for - * displaying author import options - * - * @return bool False if error uploading or invalid file, true otherwise - */ - function handle_upload() { + $this->import_start(); - if ( empty( $_POST['file_url'] ) ) { + $loop = 0; - $file = wp_import_handle_upload(); + if ( ( $handle = fopen( $file, "r" ) ) !== false ) { - if ( isset( $file['error'] ) ) { - echo '

    ' . __( 'Sorry, there has been an error.', 'woocommerce' ) . '
    '; - echo esc_html( $file['error'] ) . '

    '; - return false; + $header = fgetcsv( $handle, 0, $this->delimiter ); + + if ( 10 === sizeof( $header ) ) { + + while ( ( $row = fgetcsv( $handle, 0, $this->delimiter ) ) !== false ) { + + list( $country, $state, $postcode, $city, $rate, $name, $priority, $compound, $shipping, $class ) = $row; + + $tax_rate = array( + 'tax_rate_country' => $country, + 'tax_rate_state' => $state, + 'tax_rate' => $rate, + 'tax_rate_name' => $name, + 'tax_rate_priority' => $priority, + 'tax_rate_compound' => $compound ? 1 : 0, + 'tax_rate_shipping' => $shipping ? 1 : 0, + 'tax_rate_order' => $loop ++, + 'tax_rate_class' => $class, + ); + + $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate ); + WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, wc_clean( $postcode ) ); + WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( $city ) ); } - - $this->id = (int) $file['id']; - } else { - - if ( file_exists( ABSPATH . $_POST['file_url'] ) ) { - - $this->file_url = esc_attr( $_POST['file_url'] ); - - } else { - - echo '

    ' . __( 'Sorry, there has been an error.', 'woocommerce' ) . '

    '; - return false; - - } - + $this->import_error( __( 'The CSV is invalid.', 'woocommerce' ) ); } - return true; + fclose( $handle ); } - /** - * header function. - * - * @access public - * @return void - */ - function header() { - echo '

    '; - echo '

    ' . __( 'Import Tax Rates', 'woocommerce' ) . '

    '; + // Show Result + echo '

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

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

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

    '; + + do_action( 'import_end' ); + } + + /** + * Handles the CSV upload and initial parsing of the file to prepare for. + * displaying author import options. + * + * @return bool False if error uploading or invalid file, true otherwise + */ + public function handle_upload() { + if ( empty( $_POST['file_url'] ) ) { + + $file = wp_import_handle_upload(); + + if ( isset( $file['error'] ) ) { + $this->import_error( $file['error'] ); + } + + $this->id = absint( $file['id'] ); + + } elseif ( file_exists( ABSPATH . $_POST['file_url'] ) ) { + $this->file_url = esc_attr( $_POST['file_url'] ); + } else { + $this->import_error(); } - /** - * footer function. - * - * @access public - * @return void - */ - function footer() { - echo '
    '; + return true; + } + + /** + * Output header html. + */ + public function header() { + echo '
    '; + echo '

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

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

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

    '; + + echo '

    ' . sprintf( __( 'Tax rates need to be defined with columns in a specific order (10 columns). Click here to download a sample.', 'woocommerce' ), WC()->plugin_url() . '/dummy-data/sample_tax_rates.csv' ) . '

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

    +

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

    +

    + +

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

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

    '; + $this->footer(); + die(); + } - /** - * greet function. - * - * @access public - * @return void - */ - function greet() { - - echo '
    '; - echo '

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

    '; - - echo '

    ' . sprintf( __( 'Tax rates need to be defined with columns in a specific order (10 columns). Click here to download a sample.', 'woocommerce' ), WC()->plugin_url() . '/dummy-data/sample_tax_rates.csv' ) . '

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

    -

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

    -

    - -

    -
    - '; - } - - /** - * Added to http_request_timeout filter to force timeout at 60 seconds during import - * @param int $val - * @return int 60 - */ - function bump_request_timeout( $val ) { - return 60; - } + /** + * Added to http_request_timeout filter to force timeout at 60 seconds during import. + * + * @param int $val + * @return int 60 + */ + public function bump_request_timeout( $val ) { + return 60; } } diff --git a/includes/admin/importers/mappings/default.php b/includes/admin/importers/mappings/default.php new file mode 100644 index 00000000000..d525623b8f6 --- /dev/null +++ b/includes/admin/importers/mappings/default.php @@ -0,0 +1,105 @@ + 'id', + 'Type' => 'type', + 'SKU' => 'sku', + 'Name' => 'name', + 'Published' => 'published', + 'Is featured?' => 'featured', + 'Visibility in catalog' => 'catalog_visibility', + 'Short description' => 'short_description', + 'Description' => 'description', + 'Date sale price starts' => 'date_on_sale_from', + 'Date sale price ends' => 'date_on_sale_to', + 'Tax status' => 'tax_status', + 'Tax class' => 'tax_class', + 'In stock?' => 'stock_status', + 'Stock' => 'stock_quantity', + 'Backorders allowed?' => 'backorders', + 'Sold individually?' => 'sold_individually', + sprintf( 'Weight (%s)', $weight_unit ) => 'weight', + sprintf( 'Length (%s)', $dimension_unit ) => 'length', + sprintf( 'Width (%s)', $dimension_unit ) => 'width', + sprintf( 'Height (%s)', $dimension_unit ) => 'height', + 'Allow customer reviews?' => 'reviews_allowed', + 'Purchase note' => 'purchase_note', + 'Sale price' => 'sale_price', + 'Regular price' => 'regular_price', + 'Categories' => 'category_ids', + 'Tags' => 'tag_ids', + 'Shipping class' => 'shipping_class_id', + 'Images' => 'images', + 'Download limit' => 'download_limit', + 'Download expiry days' => 'download_expiry', + 'Parent' => 'parent_id', + 'Upsells' => 'upsell_ids', + 'Cross-sells' => 'cross_sell_ids', + 'Grouped products' => 'grouped_products', + 'External URL' => 'product_url', + 'Button text' => 'button_text', + ); + + return array_merge( $mappings, $new_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_default_english_mappings', 100 ); + +/** + * Add English special mapping placeholders when not using English as current language. + * + * @since 3.1.0 + * @param array $mappings + * @return array + */ +function wc_importer_default_special_english_mappings( $mappings ) { + if ( 'en_US' === wc_importer_current_locale() ) { + return $mappings; + } + + $new_mappings = array( + 'Attribute %d name' => 'attributes:name', + 'Attribute %d value(s)' => 'attributes:value', + 'Attribute %d visible' => 'attributes:visible', + 'Attribute %d global' => 'attributes:taxonomy', + 'Attribute %d default' => 'attributes:default', + 'Download %d name' => 'downloads:name', + 'Download %d URL' => 'downloads:url', + 'Meta: %s' => 'meta:', + ); + + return array_merge( $mappings, $new_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_default_special_english_mappings', 100 ); diff --git a/includes/admin/importers/mappings/generic.php b/includes/admin/importers/mappings/generic.php new file mode 100644 index 00000000000..bf8114758fa --- /dev/null +++ b/includes/admin/importers/mappings/generic.php @@ -0,0 +1,25 @@ + 'name', + __( 'Product Title', 'woocommerce' ) => 'name', + __( 'Price', 'woocommerce' ) => 'regular_price', + __( 'Parent SKU', 'woocommerce' ) => 'parent_id', + __( 'Quantity', 'woocommerce' ) => 'stock_quantity', + ); + + return array_merge( $mappings, $generic_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_generic_mappings' ); diff --git a/includes/admin/importers/mappings/mappings.php b/includes/admin/importers/mappings/mappings.php new file mode 100644 index 00000000000..1b965b5dc51 --- /dev/null +++ b/includes/admin/importers/mappings/mappings.php @@ -0,0 +1,12 @@ + 'id', + 'post_title' => 'name', + 'post_content' => 'description', + 'post_excerpt' => 'short_description', + 'post_parent' => 'parent_id', + ); + + return array_merge( $mappings, $wp_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_wordpress_mappings' ); diff --git a/includes/admin/importers/views/html-csv-import-done.php b/includes/admin/importers/views/html-csv-import-done.php new file mode 100644 index 00000000000..086d7cea0c7 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-done.php @@ -0,0 +1,93 @@ + +
    +
    + ' . number_format_i18n( $imported ) . '' + ); + } + + if ( 0 < $updated ) { + $results[] = sprintf( + /* translators: %d: products count */ + _n( '%s product updated', '%s products updated', $updated, 'woocommerce' ), + '' . number_format_i18n( $updated ) . '' + ); + } + + if ( 0 < $skipped ) { + $results[] = sprintf( + /* translators: %d: products count */ + _n( '%s product was skipped', '%s products were skipped', $skipped, 'woocommerce' ), + '' . number_format_i18n( $skipped ) . '' + ); + } + + if ( 0 < $failed ) { + $results [] = sprintf( + /* translators: %d: products count */ + _n( 'Failed to import %s product', 'Failed to import %s products', $failed, 'woocommerce' ), + '' . number_format_i18n( $failed ) . '' + ); + } + + if ( 0 < $failed || 0 < $skipped ) { + $results[] = '' . __( 'View import log', 'woocommerce' ) . ''; + } + + /* translators: %d: import results */ + echo wp_kses_post( __( 'Import complete!', 'woocommerce' ) . ' ' . implode( '. ', $results ) ); + ?> +
    + + +
    + +
    +
    diff --git a/includes/admin/importers/views/html-csv-import-footer.php b/includes/admin/importers/views/html-csv-import-footer.php new file mode 100644 index 00000000000..c352294a2e7 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-footer.php @@ -0,0 +1,10 @@ + +
    +
    diff --git a/includes/admin/importers/views/html-csv-import-header.php b/includes/admin/importers/views/html-csv-import-header.php new file mode 100644 index 00000000000..93bc85f6686 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-header.php @@ -0,0 +1,12 @@ + +
    +

    + +
    diff --git a/includes/admin/importers/views/html-csv-import-mapping.php b/includes/admin/importers/views/html-csv-import-mapping.php new file mode 100644 index 00000000000..33f4871786f --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-mapping.php @@ -0,0 +1,61 @@ + +
    +
    +

    +

    +
    +
    + + + + + + + + + $name ) : ?> + + + + + + + +
    + + + + + + +
    +
    +
    + + + + + +
    +
    diff --git a/includes/admin/importers/views/html-csv-import-progress.php b/includes/admin/importers/views/html-csv-import-progress.php new file mode 100644 index 00000000000..490d22a2e19 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-progress.php @@ -0,0 +1,18 @@ + +
    +
    + +

    +

    +
    +
    + +
    +
    diff --git a/includes/admin/importers/views/html-csv-import-steps.php b/includes/admin/importers/views/html-csv-import-steps.php new file mode 100644 index 00000000000..36d21f8c9d6 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-steps.php @@ -0,0 +1,19 @@ + +
      + steps as $step_key => $step ) : ?> +
    1. + +
    diff --git a/includes/admin/importers/views/html-product-csv-import-form.php b/includes/admin/importers/views/html-product-csv-import-form.php new file mode 100644 index 00000000000..16525304fc4 --- /dev/null +++ b/includes/admin/importers/views/html-product-csv-import-form.php @@ -0,0 +1,92 @@ + +
    +
    +

    +

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

    +

    +
    + + + +
    + +

    + + + +
    +
    + +
    + + + +
    +
    diff --git a/includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php b/includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php index 01eab8cea19..c5dcc97ec3d 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php @@ -4,24 +4,30 @@ * * Display the coupon data meta box. * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin/Meta Boxes * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} /** - * WC_Meta_Box_Coupon_Data + * WC_Meta_Box_Coupon_Data Class. */ class WC_Meta_Box_Coupon_Data { /** - * Output the metabox + * Output the metabox. + * + * @param WP_Post $post */ public static function output( $post ) { wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' ); + + $coupon = new WC_Coupon( $post->ID ); ?>
    - - + +
    -

    get_order_number() ) ); ?>

    +

    labels->singular_name, + $order->get_order_number() + ); + ?>

    get_title() ) : esc_html( $payment_method ) ) ); + /* translators: %s: payment method */ + printf( + __( 'Payment via %s', 'woocommerce' ), + ( isset( $payment_gateways[ $payment_method ] ) ? esc_html( $payment_gateways[ $payment_method ]->get_title() ) : esc_html( $payment_method ) ) + ); - if ( $transaction_id = get_post_meta( $order->id, '_transaction_id', true ) ) { - if ( isset( $payment_gateways[ $payment_method ] ) && ( $url = $payment_gateways[ $payment_method ]->get_transaction_url( $transaction_id ) ) ) { + if ( $transaction_id = $order->get_transaction_id() ) { + if ( isset( $payment_gateways[ $payment_method ] ) && ( $url = $payment_gateways[ $payment_method ]->get_transaction_url( $order ) ) ) { echo ' (' . esc_html( $transaction_id ) . ')'; } else { echo ' (' . esc_html( $transaction_id ) . ')'; } } + + if ( $order->get_date_paid() ) { + /* translators: 1: date 2: time */ + printf( ' ' . __( 'on %1$s @ %2$s', 'woocommerce' ), wc_format_datetime( $order->get_date_paid() ), wc_format_datetime( $order->get_date_paid(), get_option( 'time_format' ) ) ); + } + echo '. '; } - if ( $ip_address = get_post_meta( $post->ID, '_customer_ip_address', true ) ) { - echo __( 'Customer IP', 'woocommerce' ) . ': ' . esc_html( $ip_address ); + if ( $ip_address = $order->get_customer_ip_address() ) { + /* translators: %s: IP address */ + printf( + __( 'Customer IP: %s', 'woocommerce' ), + '' . esc_html( $ip_address ) . '' + ); } ?>

    -

    +

    - @: + @‎:

    -

    - $status_name ) { - echo ''; + echo ''; } ?>

    -

    - - +

    -
    -

    <?php _e( 'Edit', 'woocommerce' ); ?>

    +

    + + + + + +

    '; if ( $order->get_formatted_billing_address() ) { - echo '

    ' . __( 'Address', 'woocommerce' ) . ':' . wp_kses( $order->get_formatted_billing_address(), array( 'br' => array() ) ) . '

    '; + echo '

    ' . __( 'Address:', 'woocommerce' ) . '' . wp_kses( $order->get_formatted_billing_address(), array( 'br' => array() ) ) . '

    '; } else { - echo '

    ' . __( 'Address', 'woocommerce' ) . ': ' . __( 'No billing address set.', 'woocommerce' ) . '

    '; + echo '

    ' . __( 'Address:', 'woocommerce' ) . ' ' . __( 'No billing address set.', 'woocommerce' ) . '

    '; } foreach ( self::$billing_fields as $key => $field ) { @@ -229,41 +293,52 @@ class WC_Meta_Box_Order_Data { $field_name = 'billing_' . $key; - if ( $order->$field_name ) { - echo '

    ' . esc_html( $field['label'] ) . ': ' . make_clickable( esc_html( $order->$field_name ) ) . '

    '; + if ( is_callable( array( $order, 'get_' . $field_name ) ) ) { + $field_value = $order->{"get_$field_name"}( 'edit' ); + } else { + $field_value = $order->get_meta( '_' . $field_name ); } + + if ( 'billing_phone' === $field_name ) { + $field_value = wc_make_phone_clickable( $field_value ); + } else { + $field_value = make_clickable( esc_html( $field_value ) ); + } + + echo '

    ' . esc_html( $field['label'] ) . ': ' . wp_kses_post( $field_value ) . '

    '; } echo '
    '; // Display form - echo '

    '; + echo '
    '; foreach ( self::$billing_fields as $key => $field ) { if ( ! isset( $field['type'] ) ) { $field['type'] = 'text'; } - + if ( ! isset( $field['id'] ) ) { + $field['id'] = '_billing_' . $key; + } switch ( $field['type'] ) { case 'select' : - woocommerce_wp_select( array( 'id' => '_billing_' . $key, 'label' => $field['label'], 'options' => $field['options'] ) ); + woocommerce_wp_select( $field ); break; default : - woocommerce_wp_text_input( array( 'id' => '_billing_' . $key, 'label' => $field['label'] ) ); + woocommerce_wp_text_input( $field ); break; } } - ?>

    - +

    +

    +

    payment_gateways(); + WC()->shipping(); - // Update meta - update_post_meta( $post_id, '_customer_user', absint( $_POST['customer_user'] ) ); + // Get order object. + $order = wc_get_order( $order_id ); + $props = array(); - if ( self::$billing_fields ) { + // Create order key. + if ( ! $order->get_order_key() ) { + $props['order_key'] = 'wc_' . apply_filters( 'woocommerce_generate_order_key', uniqid( 'order_' ) ); + } + + // Update customer. + $customer_id = isset( $_POST['customer_user'] ) ? absint( $_POST['customer_user'] ) : 0; + if ( $customer_id !== $order->get_customer_id() ) { + $props['customer_id'] = $customer_id; + } + + // Update billing fields. + if ( ! empty( self::$billing_fields ) ) { foreach ( self::$billing_fields as $key => $field ) { - update_post_meta( $post_id, '_billing_' . $key, wc_clean( $_POST[ '_billing_' . $key ] ) ); + if ( ! isset( $field['id'] ) ) { + $field['id'] = '_billing_' . $key; + } + + if ( ! isset( $_POST[ $field['id'] ] ) ) { + continue; + } + + if ( is_callable( array( $order, 'set_billing_' . $key ) ) ) { + $props[ 'billing_' . $key ] = wc_clean( $_POST[ $field['id'] ] ); + } else { + $order->update_meta_data( $field['id'], wc_clean( $_POST[ $field['id'] ] ) ); + } } } - if ( self::$shipping_fields ) { + // Update shipping fields. + if ( ! empty( self::$shipping_fields ) ) { foreach ( self::$shipping_fields as $key => $field ) { - update_post_meta( $post_id, '_shipping_' . $key, wc_clean( $_POST[ '_shipping_' . $key ] ) ); + if ( ! isset( $field['id'] ) ) { + $field['id'] = '_shipping_' . $key; + } + + if ( ! isset( $_POST[ $field['id'] ] ) ) { + continue; + } + + if ( is_callable( array( $order, 'set_shipping_' . $key ) ) ) { + $props[ 'shipping_' . $key ] = wc_clean( $_POST[ $field['id'] ] ); + } else { + $order->update_meta_data( $field['id'], wc_clean( $_POST[ $field['id'] ] ) ); + } } } if ( isset( $_POST['_transaction_id'] ) ) { - update_post_meta( $post_id, '_transaction_id', wc_clean( $_POST[ '_transaction_id' ] ) ); + $props['transaction_id'] = wc_clean( $_POST['_transaction_id'] ); } - // Payment method handling - if ( get_post_meta( $post_id, '_payment_method', true ) !== stripslashes( $_POST['_payment_method'] ) ) { - + // Payment method handling. + if ( $order->get_payment_method() !== wp_unslash( $_POST['_payment_method'] ) ) { $methods = WC()->payment_gateways->payment_gateways(); $payment_method = wc_clean( $_POST['_payment_method'] ); $payment_method_title = $payment_method; - if ( isset( $methods) && isset( $methods[ $payment_method ] ) ) { + if ( isset( $methods ) && isset( $methods[ $payment_method ] ) ) { $payment_method_title = $methods[ $payment_method ]->get_title(); } - update_post_meta( $post_id, '_payment_method', $payment_method ); - update_post_meta( $post_id, '_payment_method_title', $payment_method_title ); + $props['payment_method'] = $payment_method; + $props['payment_method_title'] = $payment_method_title; } - // Update date + // Update date. if ( empty( $_POST['order_date'] ) ) { - $date = current_time('timestamp'); + $date = current_time( 'timestamp', true ); } else { - $date = strtotime( $_POST['order_date'] . ' ' . (int) $_POST['order_date_hour'] . ':' . (int) $_POST['order_date_minute'] . ':00' ); + $date = gmdate( 'Y-m-d H:i:s', strtotime( $_POST['order_date'] . ' ' . (int) $_POST['order_date_hour'] . ':' . (int) $_POST['order_date_minute'] . ':00' ) ); } - $date = date_i18n( 'Y-m-d H:i:s', $date ); + $props['date_created'] = $date; - $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_date = %s, post_date_gmt = %s WHERE ID = %s", $date, get_gmt_from_date( $date ), $post_id ) ); - - // Order data saved, now get it so we can manipulate status - $order = get_order( $post_id ); - - // Order status - $order->update_status( $_POST['order_status'] ); - - wc_delete_shop_order_transients( $post_id ); + // Save order data. + $order->set_props( $props ); + $order->set_status( wc_clean( $_POST['order_status'] ), '', true ); + $order->save(); } } diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php b/includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php index 36938265456..062fe512cad 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php @@ -2,67 +2,69 @@ /** * Order Downloads * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin/Meta Boxes * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} /** - * WC_Meta_Box_Order_Downloads + * WC_Meta_Box_Order_Downloads Class. */ class WC_Meta_Box_Order_Downloads { /** - * Output the metabox + * Output the metabox. + * + * @param WP_Post $post */ public static function output( $post ) { - global $post, $wpdb; ?>
    -
    - get_results( $wpdb->prepare( " - SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions - WHERE order_id = %d ORDER BY product_id - ", $post->ID ) ); +
    get_downloads( array( + 'order_id' => $post->ID, + 'orderby' => 'product_id', + ) ); - $product = null; - $loop = 0; - if ( $download_permissions && sizeof( $download_permissions ) > 0 ) foreach ( $download_permissions as $download ) { + $product = null; + $loop = 0; + $file_counter = 1; - if ( ! $product || $product->id != $download->product_id ) { - $product = get_product( absint( $download->product_id ) ); + if ( $download_permissions && sizeof( $download_permissions ) > 0 ) { + foreach ( $download_permissions as $download ) { + if ( ! $product || $product->get_id() !== $download->get_product_id() ) { + $product = wc_get_product( $download->get_product_id() ); $file_counter = 1; } // don't show permissions to files that have since been removed - if ( ! $product || ! $product->exists() || ! $product->has_file( $download->download_id ) ) + if ( ! $product || ! $product->exists() || ! $product->has_file( $download->get_download_id() ) ) { continue; + } // Show file title instead of count if set - $file = $product->get_file( $download->download_id ); - if ( isset( $file['name'] ) ) { - $file_count = $file['name']; - } else { - $file_count = sprintf( __( 'File %d', 'woocommerce' ), $file_counter ); - } + $file = $product->get_file( $download->get_download_id() ); + $file_count = isset( $file['name'] ) ? $file['name'] : sprintf( __( 'File %d', 'woocommerce' ), $file_counter ); include( 'views/html-order-download-permission.php' ); $loop++; $file_counter++; } - ?> -
    + } + ?>

    - - + +

    @@ -72,55 +74,27 @@ class WC_Meta_Box_Order_Downloads { } /** - * Save meta box data + * Save meta box data. + * + * @param int $post_id + * @param WP_Post $post */ public static function save( $post_id, $post ) { - global $wpdb; + if ( isset( $_POST['permission_id'] ) ) { + $permission_ids = $_POST['permission_id']; + $downloads_remaining = $_POST['downloads_remaining']; + $access_expires = $_POST['access_expires']; + $max = max( array_keys( $permission_ids ) ); - if ( isset( $_POST['download_id'] ) ) { - - // Download data - $download_ids = $_POST['download_id']; - $product_ids = $_POST['product_id']; - $downloads_remaining = $_POST['downloads_remaining']; - $access_expires = $_POST['access_expires']; - - // Order data - $order_key = get_post_meta( $post->ID, '_order_key', true ); - $customer_email = get_post_meta( $post->ID, '_billing_email', true ); - $customer_user = get_post_meta( $post->ID, '_customer_user', true ); - $product_ids_count = sizeof( $product_ids ); - - for ( $i = 0; $i < $product_ids_count; $i ++ ) { - if ( ! isset( $product_ids[ $i ] ) ) + for ( $i = 0; $i <= $max; $i ++ ) { + if ( ! isset( $permission_ids[ $i ] ) ) { continue; - - $data = array( - 'user_id' => absint( $customer_user ), - 'user_email' => wc_clean( $customer_email ), - 'downloads_remaining' => wc_clean( $downloads_remaining[ $i ] ) - ); - - $format = array( '%d', '%s', '%s' ); - - $expiry = ( array_key_exists( $i, $access_expires ) && $access_expires[ $i ] != '' ) ? date_i18n( 'Y-m-d', strtotime( $access_expires[ $i ] ) ) : null; - - if ( ! is_null( $expiry ) ) { - $data['access_expires'] = $expiry; - $format[] = '%s'; - } - - $wpdb->update( $wpdb->prefix . "woocommerce_downloadable_product_permissions", - $data, - array( - 'order_id' => $post_id, - 'product_id' => absint( $product_ids[ $i ] ), - 'download_id' => wc_clean( $download_ids[ $i ] ) - ), - $format, array( '%d', '%d', '%s' ) - ); - + } + $download = new WC_Customer_Download( $permission_ids[ $i ] ); + $download->set_downloads_remaining( wc_clean( $downloads_remaining[ $i ] ) ); + $download->set_access_expires( array_key_exists( $i, $access_expires ) && '' !== $access_expires[ $i ] ? strtotime( $access_expires[ $i ] ) : '' ); + $download->save(); } } } -} \ No newline at end of file +} diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-items.php b/includes/admin/meta-boxes/class-wc-meta-box-order-items.php index cc250e0e87e..5303d209d96 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-order-items.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-order-items.php @@ -4,179 +4,50 @@ * * Functions for displaying the order items meta box. * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin/Meta Boxes * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} +/** + * WC_Meta_Box_Order_Items Class. + */ class WC_Meta_Box_Order_Items { /** - * Output the metabox + * Output the metabox. + * + * @param WP_Post $post */ public static function output( $post ) { - global $thepostid, $theorder; + global $post, $thepostid, $theorder; - if ( ! is_object( $theorder ) ) - $theorder = get_order( $thepostid ); + if ( ! is_int( $thepostid ) ) { + $thepostid = $post->ID; + } + + if ( ! is_object( $theorder ) ) { + $theorder = wc_get_order( $thepostid ); + } $order = $theorder; - ?> -
    - - - - - + $data = get_post_meta( $post->ID ); - - - - - - - - - - - - - - - - - - - - get_items( apply_filters( 'woocommerce_admin_order_item_types', array( 'line_item', 'fee' ) ) ); - - foreach ( $order_items as $item_id => $item ) { - - switch ( $item['type'] ) { - case 'line_item' : - $_product = $order->get_product_from_item( $item ); - $item_meta = $order->get_item_meta( $item_id ); - - include( 'views/html-order-item.php' ); - break; - case 'fee' : - include( 'views/html-order-fee.php' ); - break; - } - - do_action( 'woocommerce_order_item_' . $item['type'] . '_html', $item_id, $item ); - } - ?> - -
     
    -
    - -

    - - - -

    - -

    - - - - -

    -
    - update( - $wpdb->prefix . "woocommerce_order_items", - array( 'order_item_name' => wc_clean( $order_item_name[ $item_id ] ) ), - array( 'order_item_id' => $item_id ), - array( '%s' ), - array( '%d' ) - ); - - if ( isset( $order_item_qty[ $item_id ] ) ) - wc_update_order_item_meta( $item_id, '_qty', wc_stock_amount( $order_item_qty[ $item_id ] ) ); - - if ( isset( $order_item_tax_class[ $item_id ] ) ) - wc_update_order_item_meta( $item_id, '_tax_class', wc_clean( $order_item_tax_class[ $item_id ] ) ); - - // Get values. Subtotals might not exist, in which case copy value from total field - $line_total[ $item_id ] = isset( $line_total[ $item_id ] ) ? $line_total[ $item_id ] : 0; - $line_tax[ $item_id ] = isset( $line_tax[ $item_id ] ) ? $line_tax[ $item_id ] : 0; - $line_subtotal[ $item_id ] = isset( $line_subtotal[ $item_id ] ) ? $line_subtotal[ $item_id ] : $line_total[ $item_id ]; - $line_subtotal_tax[ $item_id ] = isset( $line_subtotal_tax[ $item_id ] ) ? $line_subtotal_tax[ $item_id ] : $line_tax[ $item_id ]; - - // Update values - wc_update_order_item_meta( $item_id, '_line_subtotal', wc_format_decimal( $line_subtotal[ $item_id ] ) ); - wc_update_order_item_meta( $item_id, '_line_subtotal_tax', wc_format_decimal( $line_subtotal_tax[ $item_id ] ) ); - wc_update_order_item_meta( $item_id, '_line_total', wc_format_decimal( $line_total[ $item_id ] ) ); - wc_update_order_item_meta( $item_id, '_line_tax', wc_format_decimal( $line_tax[ $item_id ] ) ); - - // Total up - $subtotal += wc_format_decimal( $line_subtotal[ $item_id ] ); - $total += wc_format_decimal( $line_total[ $item_id ] ); - - // Clear meta cache - wp_cache_delete( $item_id, 'order_item_meta' ); - } - } - - // Save meta - $meta_keys = isset( $_POST['meta_key'] ) ? $_POST['meta_key'] : array(); - $meta_values = isset( $_POST['meta_value'] ) ? $_POST['meta_value'] : array(); - - foreach ( $meta_keys as $id => $meta_key ) { - $meta_value = ( empty( $meta_values[ $id ] ) && ! is_numeric( $meta_values[ $id ] ) ) ? '' : $meta_values[ $id ]; - $wpdb->update( - $wpdb->prefix . "woocommerce_order_itemmeta", - array( - 'meta_key' => wp_unslash($meta_key), - 'meta_value' => wp_unslash($meta_value) - ), - array( 'meta_id' => $id ), - array( '%s', '%s' ), - array( '%d' ) - ); - } - - // Update cart discount from item totals - update_post_meta( $post_id, '_cart_discount', $subtotal - $total ); + wc_save_order_items( $post_id, $_POST ); } } diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php b/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php index 49a6c2af60b..4e269154c55 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php @@ -2,29 +2,35 @@ /** * Order Notes * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin/Meta Boxes * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} /** - * WC_Meta_Box_Order_Notes + * WC_Meta_Box_Order_Notes Class. */ class WC_Meta_Box_Order_Notes { /** - * Output the metabox + * Output the metabox. + * + * @param WP_Post $post */ public static function output( $post ) { global $post; $args = array( - 'post_id' => $post->ID, - 'approve' => 'approve', - 'type' => 'order_note' + 'post_id' => $post->ID, + 'orderby' => 'comment_ID', + 'order' => 'DESC', + 'approve' => 'approve', + 'type' => 'order_note', ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); @@ -36,41 +42,51 @@ class WC_Meta_Box_Order_Notes { echo '
      '; if ( $notes ) { - foreach( $notes as $note ) { - $note_classes = get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? array( 'customer-note', 'note' ) : array( 'note' ); + foreach ( $notes as $note ) { + + $note_classes = array( 'note' ); + $note_classes[] = get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? 'customer-note' : ''; + $note_classes[] = ( __( 'WooCommerce', 'woocommerce' ) === $note->comment_author ) ? 'system-note' : ''; + $note_classes = apply_filters( 'woocommerce_order_note_class', array_filter( $note_classes ), $note ); ?> -
    • +
    • comment_content ) ) ); ?>

      - comment_date_gmt ), current_time( 'timestamp', 1 ) ) ); ?> - comment_author !== __( 'WooCommerce', 'woocommerce' ) ) printf( ' ' . __( 'by %s', 'woocommerce' ), $note->comment_author ); ?> - + comment_date ) ), date_i18n( wc_time_format(), strtotime( $note->comment_date ) ) ); ?> + comment_author ) : + /* translators: %s: note author */ + printf( ' ' . __( 'by %s', 'woocommerce' ), $note->comment_author ); + endif; + ?> +

    • ' . __( 'There are no notes for this order yet.', 'woocommerce' ) . ''; + echo '
    • ' . __( 'There are no notes yet.', 'woocommerce' ) . '
    • '; } echo '
    '; ?>
    -

    ' src="plugin_url(); ?>/assets/images/help.png" height="16" width="16" />

    +

    + - +

    comment_ID, 'rating', true ); - ?> - - ID ); - - $order = $theorder; - - $data = get_post_meta( $post->ID ); - ?> -
    -

    - -
    - shipping() ) - $shipping_methods = WC()->shipping->load_shipping_methods(); - - foreach ( $order->get_shipping_methods() as $item_id => $item ) { - $chosen_method = $item['method_id']; - $shipping_title = $item['name']; - $shipping_cost = $item['cost']; - - include( 'views/html-order-shipping.php' ); - } - - // Pre 2.1 - if ( isset( $data['_shipping_method'] ) ) { - $item_id = ''; - $chosen_method = ! empty( $data['_shipping_method'][0] ) ? $data['_shipping_method'][0] : ''; - $shipping_title = ! empty( $data['_shipping_method_title'][0] ) ? $data['_shipping_method_title'][0] : ''; - $shipping_cost = ! empty( $data['_order_shipping'][0] ) ? $data['_order_shipping'][0] : ''; - - include( 'views/html-order-shipping.php' ); - } - ?> -
    - -

    [?]

    -
    - - ID ) ?> -
    - - - -
    -

    -
    - get_results( "SELECT tax_rate_id, tax_rate_country, tax_rate_state, tax_rate_name, tax_rate_priority FROM {$wpdb->prefix}woocommerce_tax_rates ORDER BY tax_rate_name" ); - - $tax_codes = array(); - - foreach( $rates as $rate ) { - $code = array(); - - $code[] = $rate->tax_rate_country; - $code[] = $rate->tax_rate_state; - $code[] = $rate->tax_rate_name ? sanitize_title( $rate->tax_rate_name ) : 'TAX'; - $code[] = absint( $rate->tax_rate_priority ); - - $tax_codes[ $rate->tax_rate_id ] = strtoupper( implode( '-', array_filter( $code ) ) ); - } - - foreach ( $order->get_taxes() as $item_id => $item ) { - include( 'views/html-order-tax.php' ); - } - ?> -
    -

    [?]

    -
    -
    - - - -
    -

    - -
    -
    -

    - -
    - get_items( array( 'coupon' ) ); - - if ( $coupons ) { - ?> -
    -
      $item ) { - - $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' LIMIT 1;", $item['name'] ) ); - - $link = $post_id ? add_query_arg( array( 'post' => $post_id, 'action' => 'edit' ), admin_url( 'post.php' ) ) : add_query_arg( array( 's' => $item['name'], 'post_status' => 'all', 'post_type' => 'shop_coupon' ), admin_url( 'edit.php' ) ); - - echo '
    • ' . esc_html( $item['name'] ). '
    • '; - } - ?>
    -
    - -

    - - - - -

    - $value ) { - - if ( $item_id == 'new' ) { - - foreach ( $value as $new_key => $new_value ) { - $rate_id = absint( $order_taxes_rate_id[ $item_id ][ $new_key ] ); - - if ( $rate_id ) { - $rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $rate_id ) ); - $label = $rate->tax_rate_name ? $rate->tax_rate_name : WC()->countries->tax_or_vat(); - $compound = $rate->tax_rate_compound ? 1 : 0; - - $code = array(); - - $code[] = $rate->tax_rate_country; - $code[] = $rate->tax_rate_state; - $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; - $code[] = absint( $rate->tax_rate_priority ); - $code = strtoupper( implode( '-', array_filter( $code ) ) ); - } else { - $code = ''; - $label = WC()->countries->tax_or_vat(); - } - - // Add line item - $new_id = wc_add_order_item( $post_id, array( - 'order_item_name' => wc_clean( $code ), - 'order_item_type' => 'tax' - ) ); - - // Add line item meta - if ( $new_id ) { - wc_update_order_item_meta( $new_id, 'rate_id', $rate_id ); - wc_update_order_item_meta( $new_id, 'label', $label ); - wc_update_order_item_meta( $new_id, 'compound', $compound ); - - if ( isset( $order_taxes_amount[ $item_id ][ $new_key ] ) ) { - wc_update_order_item_meta( $new_id, 'tax_amount', wc_format_decimal( $order_taxes_amount[ $item_id ][ $new_key ] ) ); - - $total_tax += wc_format_decimal( $order_taxes_amount[ $item_id ][ $new_key ] ); - } - - if ( isset( $order_taxes_shipping_amount[ $item_id ][ $new_key ] ) ) { - wc_update_order_item_meta( $new_id, 'shipping_tax_amount', wc_format_decimal( $order_taxes_shipping_amount[ $item_id ][ $new_key ] ) ); - - $total_shipping_tax += wc_format_decimal( $order_taxes_shipping_amount[ $item_id ][ $new_key ] ); - } - } - } - - } else { - - $item_id = absint( $item_id ); - $rate_id = absint( $order_taxes_rate_id[ $item_id ] ); - - if ( $rate_id ) { - $rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $rate_id ) ); - $label = $rate->tax_rate_name ? $rate->tax_rate_name : WC()->countries->tax_or_vat(); - $compound = $rate->tax_rate_compound ? 1 : 0; - - $code = array(); - - $code[] = $rate->tax_rate_country; - $code[] = $rate->tax_rate_state; - $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; - $code[] = absint( $rate->tax_rate_priority ); - $code = strtoupper( implode( '-', array_filter( $code ) ) ); - } else { - $code = ''; - $label = WC()->countries->tax_or_vat(); - } - - $wpdb->update( - $wpdb->prefix . "woocommerce_order_items", - array( 'order_item_name' => wc_clean( $code ) ), - array( 'order_item_id' => $item_id ), - array( '%s' ), - array( '%d' ) - ); - - wc_update_order_item_meta( $item_id, 'rate_id', $rate_id ); - wc_update_order_item_meta( $item_id, 'label', $label ); - wc_update_order_item_meta( $item_id, 'compound', $compound ); - - if ( isset( $order_taxes_amount[ $item_id ] ) ) { - wc_update_order_item_meta( $item_id, 'tax_amount', wc_format_decimal( $order_taxes_amount[ $item_id ] ) ); - - $total_tax += wc_format_decimal( $order_taxes_amount[ $item_id ] ); - } - - if ( isset( $order_taxes_shipping_amount[ $item_id ] ) ) { - wc_update_order_item_meta( $item_id, 'shipping_tax_amount', wc_format_decimal( $order_taxes_shipping_amount[ $item_id ] ) ); - - $total_shipping_tax += wc_format_decimal( $order_taxes_shipping_amount[ $item_id ] ); - } - } - } - } - - // Update totals - update_post_meta( $post_id, '_order_tax', wc_format_decimal( $total_tax ) ); - update_post_meta( $post_id, '_order_shipping_tax', wc_format_decimal( $total_shipping_tax ) ); - update_post_meta( $post_id, '_order_discount', wc_format_decimal( $_POST['_order_discount'] ) ); - update_post_meta( $post_id, '_order_total', wc_format_decimal( $_POST['_order_total'] ) ); - - // Shipping Rows - $order_shipping = 0; - - if ( isset( $_POST['shipping_method_id'] ) ) { - - $get_values = array( 'shipping_method_id', 'shipping_method_title', 'shipping_method', 'shipping_cost' ); - - foreach( $get_values as $value ) - $$value = isset( $_POST[ $value ] ) ? $_POST[ $value ] : array(); - - foreach( $shipping_method_id as $item_id => $value ) { - - if ( $item_id == 'new' ) { - - foreach ( $value as $new_key => $new_value ) { - $method_id = wc_clean( $shipping_method[ $item_id ][ $new_key ] ); - $method_title = wc_clean( $shipping_method_title[ $item_id ][ $new_key ] ); - $cost = wc_format_decimal( $shipping_cost[ $item_id ][ $new_key ] ); - - $new_id = wc_add_order_item( $post_id, array( - 'order_item_name' => $method_title, - 'order_item_type' => 'shipping' - ) ); - - if ( $new_id ) { - wc_add_order_item_meta( $new_id, 'method_id', $method_id ); - wc_add_order_item_meta( $new_id, 'cost', $cost ); - } - - $order_shipping += $cost; - } - - } else { - - $item_id = absint( $item_id ); - $method_id = wc_clean( $shipping_method[ $item_id ] ); - $method_title = wc_clean( $shipping_method_title[ $item_id ] ); - $cost = wc_format_decimal( $shipping_cost[ $item_id ] ); - - $wpdb->update( - $wpdb->prefix . "woocommerce_order_items", - array( 'order_item_name' => $method_title ), - array( 'order_item_id' => $item_id ), - array( '%s' ), - array( '%d' ) - ); - - wc_update_order_item_meta( $item_id, 'method_id', $method_id ); - wc_update_order_item_meta( $item_id, 'cost', $cost ); - - $order_shipping += $cost; - } - } - } - - // Delete rows - if ( isset( $_POST['delete_order_item_id'] ) ) { - $delete_ids = $_POST['delete_order_item_id']; - - foreach ( $delete_ids as $id ) - wc_delete_order_item( absint( $id ) ); - } - - delete_post_meta( $post_id, '_shipping_method' ); - delete_post_meta( $post_id, '_shipping_method_title' ); - update_post_meta( $post_id, '_order_shipping', $order_shipping ); - add_post_meta( $post_id, '_order_currency', get_woocommerce_currency(), true ); - } -} \ No newline at end of file diff --git a/includes/admin/meta-boxes/class-wc-meta-box-product-data.php b/includes/admin/meta-boxes/class-wc-meta-box-product-data.php index a7d9e32d354..d92750d8008 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-product-data.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-product-data.php @@ -4,1662 +4,433 @@ * * Displays the product data box, tabbed, with several panels covering price, stock etc. * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin/Meta Boxes - * @version 2.1.0 + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin/Meta Boxes + * @version 3.0.0 */ if ( ! defined( 'ABSPATH' ) ) { - exit; // Exit if accessed directly + exit; } /** - * WC_Meta_Box_Product_Data + * WC_Meta_Box_Product_Data Class. */ class WC_Meta_Box_Product_Data { /** - * Output the metabox + * Output the metabox. + * + * @param WP_Post $post */ public static function output( $post ) { - global $post, $thepostid; + global $thepostid, $product_object; - wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' ); + $thepostid = $post->ID; + $product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product; - $thepostid = $post->ID; + include( 'views/html-product-data-panel.php' ); + } - if ( $terms = wp_get_object_terms( $post->ID, 'product_type' ) ) { - $product_type = sanitize_title( current( $terms )->name ); - } else { - $product_type = apply_filters( 'default_product_type', 'simple' ); - } + /** + * Show tab content/settings. + */ + private static function output_tabs() { + global $post, $thepostid, $product_object; - $product_type_selector = apply_filters( 'product_type_selector', array( - 'simple' => __( 'Simple product', 'woocommerce' ), - 'grouped' => __( 'Grouped product', 'woocommerce' ), - 'external' => __( 'External/Affiliate product', 'woocommerce' ), - 'variable' => __( 'Variable product', 'woocommerce' ) - ), $product_type ); + include( 'views/html-product-data-general.php' ); + include( 'views/html-product-data-inventory.php' ); + include( 'views/html-product-data-shipping.php' ); + include( 'views/html-product-data-linked-products.php' ); + include( 'views/html-product-data-attributes.php' ); + include( 'views/html-product-data-advanced.php' ); + } - $type_box = ''; - - $product_type_options = apply_filters( 'product_type_options', array( + /** + * Return array of product type options. + * @return array + */ + private static function get_product_type_options() { + return apply_filters( 'product_type_options', array( 'virtual' => array( 'id' => '_virtual', 'wrapper_class' => 'show_if_simple', 'label' => __( 'Virtual', 'woocommerce' ), - 'description' => __( 'Virtual products are intangible and aren\'t shipped.', 'woocommerce' ), - 'default' => 'no' + 'description' => __( 'Virtual products are intangible and are not shipped.', 'woocommerce' ), + 'default' => 'no', ), 'downloadable' => array( 'id' => '_downloadable', 'wrapper_class' => 'show_if_simple', 'label' => __( 'Downloadable', 'woocommerce' ), 'description' => __( 'Downloadable products give access to a file upon purchase.', 'woocommerce' ), - 'default' => 'no' - ) + 'default' => 'no', + ), + ) ); + } + + /** + * Return array of tabs to show. + * @return array + */ + private static function get_product_data_tabs() { + $tabs = apply_filters( 'woocommerce_product_data_tabs', array( + 'general' => array( + 'label' => __( 'General', 'woocommerce' ), + 'target' => 'general_product_data', + 'class' => array( 'hide_if_grouped' ), + 'priority' => 10, + ), + 'inventory' => array( + 'label' => __( 'Inventory', 'woocommerce' ), + 'target' => 'inventory_product_data', + 'class' => array( 'show_if_simple', 'show_if_variable', 'show_if_grouped', 'show_if_external' ), + 'priority' => 20, + ), + 'shipping' => array( + 'label' => __( 'Shipping', 'woocommerce' ), + 'target' => 'shipping_product_data', + 'class' => array( 'hide_if_virtual', 'hide_if_grouped', 'hide_if_external' ), + 'priority' => 30, + ), + 'linked_product' => array( + 'label' => __( 'Linked Products', 'woocommerce' ), + 'target' => 'linked_product_data', + 'class' => array(), + 'priority' => 40, + ), + 'attribute' => array( + 'label' => __( 'Attributes', 'woocommerce' ), + 'target' => 'product_attributes', + 'class' => array(), + 'priority' => 50, + ), + 'variations' => array( + 'label' => __( 'Variations', 'woocommerce' ), + 'target' => 'variable_product_options', + 'class' => array( 'variations_tab', 'show_if_variable' ), + 'priority' => 60, + ), + 'advanced' => array( + 'label' => __( 'Advanced', 'woocommerce' ), + 'target' => 'advanced_product_data', + 'class' => array(), + 'priority' => 70, + ), ) ); - foreach ( $product_type_options as $key => $option ) { - $selected_value = get_post_meta( $post->ID, '_' . $key, true ); + // Sort tabs based on priority. + uasort( $tabs, array( __CLASS__, 'product_data_tabs_sort' ) ); - if ( '' == $selected_value && isset( $option['default'] ) ) { - $selected_value = $option['default']; - } - - $type_box .= ''; - } - - ?> -
    - - - -
    - - -
    '; - - // SKU - if ( wc_product_sku_enabled() ) { - woocommerce_wp_text_input( array( 'id' => '_sku', 'label' => '' . __( 'SKU', 'woocommerce' ) . '', 'desc_tip' => 'true', 'description' => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ) ) ); - } else { - echo ''; - } - - do_action( 'woocommerce_product_options_sku' ); - - echo '
    '; - - echo '
    '; - - // External URL - woocommerce_wp_text_input( array( 'id' => '_product_url', 'label' => __( 'Product URL', 'woocommerce' ), 'placeholder' => 'http://', 'description' => __( 'Enter the external URL to the product.', 'woocommerce' ) ) ); - - // Button text - woocommerce_wp_text_input( array( 'id' => '_button_text', 'label' => __( 'Button text', 'woocommerce' ), 'placeholder' => _x('Buy product', 'placeholder', 'woocommerce'), 'description' => __( 'This text will be shown on the button linking to the external product.', 'woocommerce' ) ) ); - - echo '
    '; - - echo '
    '; - - // Price - woocommerce_wp_text_input( array( 'id' => '_regular_price', 'label' => __( 'Regular Price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')', 'data_type' => 'price' ) ); - - // Special Price - woocommerce_wp_text_input( array( 'id' => '_sale_price', 'data_type' => 'price', 'label' => __( 'Sale Price', 'woocommerce' ) . ' ('.get_woocommerce_currency_symbol().')', 'description' => '' . __( 'Schedule', 'woocommerce' ) . '' ) ); - - // Special Price date range - $sale_price_dates_from = ( $date = get_post_meta( $thepostid, '_sale_price_dates_from', true ) ) ? date_i18n( 'Y-m-d', $date ) : ''; - $sale_price_dates_to = ( $date = get_post_meta( $thepostid, '_sale_price_dates_to', true ) ) ? date_i18n( 'Y-m-d', $date ) : ''; - - echo '

    - - - - '. __( 'Cancel', 'woocommerce' ) .' -

    '; - - do_action( 'woocommerce_product_options_pricing' ); - - echo '
    '; - - echo '
    '; - - ?> -
    - - - - - - - - - - - - ID, '_downloadable_files', true ); - - if ( $downloadable_files ) { - foreach ( $downloadable_files as $key => $file ) { - include( 'views/html-product-download.php' ); - } - } - ?> - - - - - - -
      [?] [?] 
    - -
    -
    - '_download_limit', 'label' => __( 'Download Limit', 'woocommerce' ), 'placeholder' => __( 'Unlimited', 'woocommerce' ), 'description' => __( 'Leave blank for unlimited re-downloads.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( - 'step' => '1', - 'min' => '0' - ) ) ); - - // Expirey - woocommerce_wp_text_input( array( 'id' => '_download_expiry', 'label' => __( 'Download Expiry', 'woocommerce' ), 'placeholder' => __( 'Never', 'woocommerce' ), 'description' => __( 'Enter the number of days before a download link expires, or leave blank.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( - 'step' => '1', - 'min' => '0' - ) ) ); - - // Download Type - woocommerce_wp_select( array( 'id' => '_download_type', 'label' => __( 'Download Type', 'woocommerce' ), 'description' => sprintf( __( 'Choose a download type - this controls the schema.', 'woocommerce' ), 'http://schema.org/' ), 'options' => array( - '' => __( 'Standard Product', 'woocommerce' ), - 'application' => __( 'Application/Software', 'woocommerce' ), - 'music' => __( 'Music', 'woocommerce' ), - ) ) ); - - do_action( 'woocommerce_product_options_downloads' ); - - echo '
    '; - - if ( 'yes' == get_option( 'woocommerce_calc_taxes' ) ) { - - echo '
    '; - - // Tax - woocommerce_wp_select( array( 'id' => '_tax_status', 'label' => __( 'Tax Status', 'woocommerce' ), 'options' => array( - 'taxable' => __( 'Taxable', 'woocommerce' ), - 'shipping' => __( 'Shipping only', 'woocommerce' ), - 'none' => _x( 'None', 'Tax status', 'woocommerce' ) - ) ) ); - - $tax_classes = array_filter( array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ) ); - $classes_options = array(); - $classes_options[''] = __( 'Standard', 'woocommerce' ); - if ( $tax_classes ) { - foreach ( $tax_classes as $class ) { - $classes_options[ sanitize_title( $class ) ] = esc_html( $class ); - } - } - - woocommerce_wp_select( array( 'id' => '_tax_class', 'label' => __( 'Tax Class', 'woocommerce' ), 'options' => $classes_options ) ); - - do_action( 'woocommerce_product_options_tax' ); - - echo '
    '; - - } - - do_action( 'woocommerce_product_options_general_product_data' ); - ?> -
    - -
    - - '; - - if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { - - // manage stock - woocommerce_wp_checkbox( array( 'id' => '_manage_stock', 'wrapper_class' => 'show_if_simple show_if_variable', 'label' => __( 'Manage stock?', 'woocommerce' ), 'description' => __( 'Enable stock management at product level', 'woocommerce' ) ) ); - - do_action( 'woocommerce_product_options_stock' ); - - echo '
    '; - - // Stock - woocommerce_wp_text_input( array( 'id' => '_stock', 'label' => __( 'Stock Qty', 'woocommerce' ), 'desc_tip' => true, 'description' => __( 'Stock quantity. If this is a variable product this value will be used to control stock for all variations, unless you define stock at variation level.', 'woocommerce' ), 'type' => 'number', 'custom_attributes' => array( - 'step' => 'any' - ) ) ); - - // Backorders? - woocommerce_wp_select( array( 'id' => '_backorders', 'label' => __( 'Allow Backorders?', 'woocommerce' ), 'options' => array( - 'no' => __( 'Do not allow', 'woocommerce' ), - 'notify' => __( 'Allow, but notify customer', 'woocommerce' ), - 'yes' => __( 'Allow', 'woocommerce' ) - ), 'desc_tip' => true, 'description' => __( 'If managing stock, this controls whether or not backorders are allowed. If enabled, stock quantity can go below 0.', 'woocommerce' ) ) ); - - do_action( 'woocommerce_product_options_stock_fields' ); - - echo '
    '; - - } - - // Stock status - woocommerce_wp_select( array( 'id' => '_stock_status', 'wrapper_class' => 'hide_if_variable', 'label' => __( 'Stock status', 'woocommerce' ), 'options' => array( - 'instock' => __( 'In stock', 'woocommerce' ), - 'outofstock' => __( 'Out of stock', 'woocommerce' ) - ), 'desc_tip' => true, 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ) ) ); - - do_action( 'woocommerce_product_options_stock_status' ); - - echo '
    '; - - echo '
    '; - - // Individual product - woocommerce_wp_checkbox( array( 'id' => '_sold_individually', 'wrapper_class' => 'show_if_simple show_if_variable', 'label' => __( 'Sold Individually', 'woocommerce' ), 'description' => __( 'Enable this to only allow one of this item to be bought in a single order', 'woocommerce' ) ) ); - - do_action( 'woocommerce_product_options_sold_individually' ); - - echo '
    '; - - do_action( 'woocommerce_product_options_inventory_product_data' ); - ?> - -
    - -
    - - '; - - // Weight - if ( wc_product_weight_enabled() ) { - woocommerce_wp_text_input( array( 'id' => '_weight', 'label' => __( 'Weight', 'woocommerce' ) . ' (' . get_option( 'woocommerce_weight_unit' ) . ')', 'placeholder' => wc_format_localized_decimal( 0 ), 'desc_tip' => 'true', 'description' => __( 'Weight in decimal form', 'woocommerce' ), 'type' => 'text', 'data_type' => 'decimal' ) ); - } - - // Size fields - if ( wc_product_dimensions_enabled() ) { - ?>

    - - - - - - - -

    '; - - echo '
    '; - - // Shipping Class - $classes = get_the_terms( $thepostid, 'product_shipping_class' ); - if ( $classes && ! is_wp_error( $classes ) ) { - $current_shipping_class = current( $classes )->term_id; - } else { - $current_shipping_class = ''; - } - - $args = array( - 'taxonomy' => 'product_shipping_class', - 'hide_empty' => 0, - 'show_option_none' => __( 'No shipping class', 'woocommerce' ), - 'name' => 'product_shipping_class', - 'id' => 'product_shipping_class', - 'selected' => $current_shipping_class, - 'class' => 'select short' - ); - ?>

    '; - ?> - -
    - -
    - -

    - -

    - -
    - - attribute_name ); - - // Ensure it exists - if ( ! taxonomy_exists( $attribute_taxonomy_name ) ) { - continue; - } - - $i++; - - // Get product data values for current taxonomy - this contains ordering and visibility data - if ( isset( $attributes[ sanitize_title( $attribute_taxonomy_name ) ] ) ) { - $attribute = $attributes[ sanitize_title( $attribute_taxonomy_name ) ]; - } - - $position = empty( $attribute['position'] ) ? 0 : absint( $attribute['position'] ); - - // Get terms of this taxonomy associated with current product - $post_terms = wp_get_post_terms( $thepostid, $attribute_taxonomy_name ); - - // Any set? - $has_terms = ( is_wp_error( $post_terms ) || ! $post_terms || sizeof( $post_terms ) == 0 ) ? 0 : 1; - ?> -
    > -

    - -
    - attribute_label ? $tax->attribute_label : $tax->attribute_name, $tax->attribute_name ); ?> -

    - - - - - - - - - - - - - -
    - - attribute_label ? $tax->attribute_label : $tax->attribute_name; ?> - - - - - - - attribute_type ) : ?> - - - - - - - attribute_type ) : ?> - - - -
    - -
    -
    - -
    -
    -
    - -
    -

    - -
    - -

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

    - - - - -

    -
    -
    - -
    - -

    - ' src="plugin_url(); ?>/assets/images/help.png" height="16" width="16" />

    - -

    - ' src="plugin_url(); ?>/assets/images/help.png" height="16" width="16" />

    - -
    - - '; - - // List Grouped products - $post_parents = array(); - $post_parents[''] = __( 'Choose a grouped product…', 'woocommerce' ); - - if ( $grouped_term = get_term_by( 'slug', 'grouped', 'product_type' ) ) { - - $posts_in = array_unique( (array) get_objects_in_term( $grouped_term->term_id, 'product_type' ) ); - if ( sizeof( $posts_in ) > 0 ) { - $args = array( - 'post_type' => 'product', - 'post_status' => 'any', - 'numberposts' => -1, - 'orderby' => 'title', - 'order' => 'asc', - 'post_parent' => 0, - 'suppress_filters' => 0, - 'include' => $posts_in, - ); - $grouped_products = get_posts( $args ); - - if ( $grouped_products ) { - foreach ( $grouped_products as $product ) { - - if ( $product->ID == $post->ID ) { - continue; - } - - $post_parents[ $product->ID ] = $product->post_title; - } - } - } - - } - - woocommerce_wp_select( array( 'id' => 'parent_id', 'label' => __( 'Grouping', 'woocommerce' ), 'value' => absint( $post->post_parent ), 'options' => $post_parents, 'desc_tip' => true, 'description' => __( 'Set this option to make this product part of a grouped product.', 'woocommerce' ) ) ); - - woocommerce_wp_hidden_input( array( 'id' => 'previous_parent_id', 'value' => absint( $post->post_parent ) ) ); - - do_action( 'woocommerce_product_options_grouping' ); - - echo '
    '; - ?> - - - -
    - -
    - - '; - - // Purchase note - woocommerce_wp_textarea_input( array( 'id' => '_purchase_note', 'label' => __( 'Purchase Note', 'woocommerce' ), 'desc_tip' => 'true', 'description' => __( 'Enter an optional note to send the customer after purchase.', 'woocommerce' ) ) ); - - echo '
    '; - - echo '
    '; - - // menu_order - woocommerce_wp_text_input( array( 'id' => 'menu_order', 'label' => __( 'Menu order', 'woocommerce' ), 'desc_tip' => 'true', 'description' => __( 'Custom ordering position.', 'woocommerce' ), 'value' => intval( $post->menu_order ), 'type' => 'number', 'custom_attributes' => array( - 'step' => '1' - ) ) ); - - echo '
    '; - - echo '
    '; - - woocommerce_wp_checkbox( array( 'id' => 'comment_status', 'label' => __( 'Enable reviews', 'woocommerce' ), 'cbvalue' => 'open', 'value' => esc_attr( $post->comment_status ) ) ); - - do_action( 'woocommerce_product_options_reviews' ); - - echo '
    '; - ?> - -
    - - - -
    - -
    - get_variation(); + } + + /** + * Show options for the variable product type. */ public static function output_variations() { - global $post; + global $post, $wpdb, $product_object; - $attributes = maybe_unserialize( get_post_meta( $post->ID, '_product_attributes', true ) ); + $variation_attributes = array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_variation_attributes' ) ); + $default_attributes = $product_object->get_default_attributes(); + $variations_count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation' AND post_status IN ('publish', 'private')", $post->ID ) ) ); + $variations_per_page = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ); + $variations_total_pages = ceil( $variations_count / $variations_per_page ); - // See if any are set - $variation_attribute_found = false; - if ( $attributes ) { - foreach ( $attributes as $attribute ) { - if ( isset( $attribute['is_variation'] ) ) { - $variation_attribute_found = true; - break; - } - } - } - - // Get tax classes - $tax_classes = array_filter( array_map('trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ) ); - $tax_class_options = array(); - $tax_class_options[''] = __( 'Standard', 'woocommerce' ); - if ( $tax_classes ) { - foreach ( $tax_classes as $class ) { - $tax_class_options[ sanitize_title( $class ) ] = esc_attr( $class ); - } - } - - $backorder_options = array( - 'no' => __( 'Do not allow', 'woocommerce' ), - 'notify' => __( 'Allow, but notify customer', 'woocommerce' ), - 'yes' => __( 'Allow', 'woocommerce' ) - ); - - $stock_status_options = array( - 'instock' => __( 'In stock', 'woocommerce' ), - 'outofstock' => __( 'Out of stock', 'woocommerce' ) - ); - - ?> -
    - - - -
    -

    Attributes tab.', 'woocommerce' ); ?>

    - -

    -
    - - - -

    - - - -

    - -
    - $post->ID, - 'attributes' => $attributes, - 'tax_class_options' => $tax_class_options, - 'sku' => get_post_meta( $post->ID, '_sku', true ), - 'weight' => wc_format_localized_decimal( get_post_meta( $post->ID, '_weight', true ) ), - 'length' => wc_format_localized_decimal( get_post_meta( $post->ID, '_length', true ) ), - 'width' => wc_format_localized_decimal( get_post_meta( $post->ID, '_width', true ) ), - 'height' => wc_format_localized_decimal( get_post_meta( $post->ID, '_height', true ) ), - 'tax_class' => get_post_meta( $post->ID, '_tax_class', true ), - 'backorder_options' => $backorder_options, - 'stock_status_options' => $stock_status_options - ); - - if ( ! $parent_data['weight'] ) { - $parent_data['weight'] = wc_format_localized_decimal( 0 ); - } - - if ( ! $parent_data['length'] ) { - $parent_data['length'] = wc_format_localized_decimal( 0 ); - } - - if ( ! $parent_data['width'] ) { - $parent_data['width'] = wc_format_localized_decimal( 0 ); - } - - if ( ! $parent_data['height'] ) { - $parent_data['height'] = wc_format_localized_decimal( 0 ); - } - - // Get variations - $args = array( - 'post_type' => 'product_variation', - 'post_status' => array( 'private', 'publish' ), - 'numberposts' => -1, - 'orderby' => 'menu_order', - 'order' => 'asc', - 'post_parent' => $post->ID - ); - $variations = get_posts( $args ); - $loop = 0; - if ( $variations ) { - foreach ( $variations as $variation ) { - - $variation_id = absint( $variation->ID ); - $variation_post_status = esc_attr( $variation->post_status ); - $variation_data = get_post_meta( $variation_id ); - $variation_data['variation_post_id'] = $variation_id; - - // Grab shipping classes - $shipping_classes = get_the_terms( $variation_id, 'product_shipping_class' ); - $shipping_class = ( $shipping_classes && ! is_wp_error( $shipping_classes ) ) ? current( $shipping_classes )->term_id : ''; - - $variation_fields = array( - '_sku', - '_stock', - '_regular_price', - '_sale_price', - '_weight', - '_length', - '_width', - '_height', - '_download_limit', - '_download_expiry', - '_downloadable_files', - '_downloadable', - '_virtual', - '_thumbnail_id', - '_sale_price_dates_from', - '_sale_price_dates_to', - '_manage_stock', - '_stock_status' - ); - - foreach ( $variation_fields as $field ) { - $$field = isset( $variation_data[ $field ][0] ) ? maybe_unserialize( $variation_data[ $field ][0] ) : ''; - } - - $_backorders = isset( $variation_data['_backorders'][0] ) ? $variation_data['_backorders'][0] : null; - $_tax_class = isset( $variation_data['_tax_class'][0] ) ? $variation_data['_tax_class'][0] : null; - $image_id = absint( $_thumbnail_id ); - $image = $image_id ? wp_get_attachment_thumb_url( $image_id ) : ''; - - // Locale formatting - $_regular_price = wc_format_localized_price( $_regular_price ); - $_sale_price = wc_format_localized_price( $_sale_price ); - $_weight = wc_format_localized_decimal( $_weight ); - $_length = wc_format_localized_decimal( $_length ); - $_width = wc_format_localized_decimal( $_width ); - $_height = wc_format_localized_decimal( $_height ); - - // Stock BW compat - if ( '' !== $_stock ) { - $_manage_stock = 'yes'; - } - - include( 'views/html-variation-admin.php' ); - - $loop++; - } - } - ?> -
    - -

    - - - - - - : [?] - ID, '_default_attributes', true ) ); - foreach ( $attributes as $attribute ) { - - // Only deal with attributes that are variations - if ( ! $attribute['is_variation'] ) { - continue; - } - - // Get current value for variation (if set) - $variation_selected_value = isset( $default_attributes[ sanitize_title( $attribute['name'] ) ] ) ? $default_attributes[ sanitize_title( $attribute['name'] ) ] : ''; - - // Name will be something like attribute_pa_color - echo ''; - } - ?> -

    - - -
    - 0 && $product_type != 'external' ? absint( $_POST['product_shipping_class'] ) : ''; - wp_set_object_terms( $post_id, $product_shipping_class, 'product_shipping_class'); - - // Unique SKU - $sku = get_post_meta( $post_id, '_sku', true ); - $new_sku = wc_clean( stripslashes( $_POST['_sku'] ) ); - - if ( '' == $new_sku ) { - update_post_meta( $post_id, '_sku', '' ); - } elseif ( $new_sku !== $sku ) { - if ( ! empty( $new_sku ) ) { - $unique_sku = wc_product_has_unique_sku( $post_id, $new_sku ); - if ( ! $unique_sku ) { - WC_Admin_Meta_Boxes::add_error( __( 'Product SKU must be unique.', 'woocommerce' ) ); - } else { - update_post_meta( $post_id, '_sku', $new_sku ); + for ( $i = 0; $i < $file_url_size; $i ++ ) { + if ( ! empty( $file_urls[ $i ] ) ) { + $downloads[] = array( + 'name' => wc_clean( $file_names[ $i ] ), + 'file' => wp_unslash( trim( $file_urls[ $i ] ) ), + 'previous_hash' => wc_clean( $file_hashes[ $i ] ), + ); } - } else { - update_post_meta( $post_id, '_sku', '' ); } } + return $downloads; + } - // Save Attributes + /** + * Prepare children for save. + * @return array + */ + private static function prepare_children() { + return isset( $_POST['grouped_products'] ) ? array_filter( array_map( 'intval', (array) $_POST['grouped_products'] ) ) : array(); + } + + /** + * Prepare attributes for save. + * + * @param array $data + * + * @return array + */ + public static function prepare_attributes( $data = false ) { $attributes = array(); - if ( isset( $_POST['attribute_names'] ) && isset( $_POST['attribute_values'] ) ) { - $attribute_names = $_POST['attribute_names']; - $attribute_values = $_POST['attribute_values']; + if ( ! $data ) { + $data = $_POST; + } - if ( isset( $_POST['attribute_visibility'] ) ) { - $attribute_visibility = $_POST['attribute_visibility']; - } + if ( isset( $data['attribute_names'], $data['attribute_values'] ) ) { + $attribute_names = $data['attribute_names']; + $attribute_values = $data['attribute_values']; + $attribute_visibility = isset( $data['attribute_visibility'] ) ? $data['attribute_visibility'] : array(); + $attribute_variation = isset( $data['attribute_variation'] ) ? $data['attribute_variation'] : array(); + $attribute_position = $data['attribute_position']; + $attribute_names_max_key = max( array_keys( $attribute_names ) ); - if ( isset( $_POST['attribute_variation'] ) ) { - $attribute_variation = $_POST['attribute_variation']; - } + for ( $i = 0; $i <= $attribute_names_max_key; $i++ ) { + if ( empty( $attribute_names[ $i ] ) || ! isset( $attribute_values[ $i ] ) ) { + continue; + } + $attribute_id = 0; + $attribute_name = wc_clean( $attribute_names[ $i ] ); - $attribute_is_taxonomy = $_POST['attribute_is_taxonomy']; - $attribute_position = $_POST['attribute_position']; - $attribute_names_count = sizeof( $attribute_names ); + if ( 'pa_' === substr( $attribute_name, 0, 3 ) ) { + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name ); + } - for ( $i = 0; $i < $attribute_names_count; $i++ ) { - if ( ! $attribute_names[ $i ] ) { + $options = isset( $attribute_values[ $i ] ) ? $attribute_values[ $i ] : ''; + + if ( is_array( $options ) ) { + // Term ids sent as array. + $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 = wc_get_text_attributes( $options ); + } + + if ( empty( $options ) ) { continue; } - $is_visible = isset( $attribute_visibility[ $i ] ) ? 1 : 0; - $is_variation = isset( $attribute_variation[ $i ] ) ? 1 : 0; - $is_taxonomy = $attribute_is_taxonomy[ $i ] ? 1 : 0; - - if ( $is_taxonomy ) { - - if ( isset( $attribute_values[ $i ] ) ) { - - // Select based attributes - Format values (posted values are slugs) - if ( is_array( $attribute_values[ $i ] ) ) { - $values = array_map( 'sanitize_title', $attribute_values[ $i ] ); - - // Text based attributes - Posted values are term names - don't change to slugs - } else { - $values = array_map( 'stripslashes', array_map( 'strip_tags', explode( WC_DELIMITER, $attribute_values[ $i ] ) ) ); - } - - // Remove empty items in the array - $values = array_filter( $values, 'strlen' ); - - } else { - $values = array(); - } - - // Update post terms - if ( taxonomy_exists( $attribute_names[ $i ] ) ) { - wp_set_object_terms( $post_id, $values, $attribute_names[ $i ] ); - } - - if ( $values ) { - // Add attribute to array, but don't set values - $attributes[ sanitize_title( $attribute_names[ $i ] ) ] = array( - 'name' => wc_clean( $attribute_names[ $i ] ), - 'value' => '', - 'position' => $attribute_position[ $i ], - 'is_visible' => $is_visible, - 'is_variation' => $is_variation, - 'is_taxonomy' => $is_taxonomy - ); - } - - } elseif ( isset( $attribute_values[ $i ] ) ) { - - // Text based, separate by pipe - $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute_values[ $i ] ) ) ); - - // Custom attribute - Add attribute to array and set the values - $attributes[ sanitize_title( $attribute_names[ $i ] ) ] = array( - 'name' => wc_clean( $attribute_names[ $i ] ), - 'value' => $values, - 'position' => $attribute_position[ $i ], - 'is_visible' => $is_visible, - 'is_variation' => $is_variation, - 'is_taxonomy' => $is_taxonomy - ); - } - - } - } - - if ( ! function_exists( 'attributes_cmp' ) ) { - function attributes_cmp( $a, $b ) { - if ( $a['position'] == $b['position'] ) { - return 0; - } - - return ( $a['position'] < $b['position'] ) ? -1 : 1; + $attribute = new WC_Product_Attribute(); + $attribute->set_id( $attribute_id ); + $attribute->set_name( $attribute_name ); + $attribute->set_options( $options ); + $attribute->set_position( $attribute_position[ $i ] ); + $attribute->set_visible( isset( $attribute_visibility[ $i ] ) ); + $attribute->set_variation( isset( $attribute_variation[ $i ] ) ); + $attributes[] = $attribute; } } - uasort( $attributes, 'attributes_cmp' ); - - update_post_meta( $post_id, '_product_attributes', $attributes ); - - // Sales and prices - if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { - - // Variable and grouped products have no prices - update_post_meta( $post_id, '_regular_price', '' ); - update_post_meta( $post_id, '_sale_price', '' ); - update_post_meta( $post_id, '_sale_price_dates_from', '' ); - update_post_meta( $post_id, '_sale_price_dates_to', '' ); - update_post_meta( $post_id, '_price', '' ); - - } else { - - $date_from = isset( $_POST['_sale_price_dates_from'] ) ? $_POST['_sale_price_dates_from'] : ''; - $date_to = isset( $_POST['_sale_price_dates_to'] ) ? $_POST['_sale_price_dates_to'] : ''; - - // Dates - if ( $date_from ) { - update_post_meta( $post_id, '_sale_price_dates_from', strtotime( $date_from ) ); - } else { - update_post_meta( $post_id, '_sale_price_dates_from', '' ); - } - - if ( $date_to ) { - update_post_meta( $post_id, '_sale_price_dates_to', strtotime( $date_to ) ); - } else { - update_post_meta( $post_id, '_sale_price_dates_to', '' ); - } - - if ( $date_to && ! $date_from ) { - update_post_meta( $post_id, '_sale_price_dates_from', strtotime( 'NOW', current_time( 'timestamp' ) ) ); - } - - // Update price if on sale - if ( '' !== $_POST['_sale_price'] && '' == $date_to && '' == $date_from ) { - update_post_meta( $post_id, '_price', wc_format_decimal( $_POST['_sale_price'] ) ); - } else { - update_post_meta( $post_id, '_price', ( $_POST['_regular_price'] === '' ) ? '' : wc_format_decimal( $_POST['_regular_price'] ) ); - } - - if ( '' !== $_POST['_sale_price'] && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $post_id, '_price', wc_format_decimal( $_POST['_sale_price'] ) ); - } - - if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $post_id, '_price', ( $_POST['_regular_price'] === '' ) ? '' : wc_format_decimal( $_POST['_regular_price'] ) ); - update_post_meta( $post_id, '_sale_price_dates_from', '' ); - update_post_meta( $post_id, '_sale_price_dates_to', '' ); - } - } - - // Update parent if grouped so price sorting works and stays in sync with the cheapest child - if ( $post->post_parent > 0 || 'grouped' == $product_type || $_POST['previous_parent_id'] > 0 ) { - - $clear_parent_ids = array(); - - if ( $post->post_parent > 0 ) { - $clear_parent_ids[] = $post->post_parent; - } - - if ( 'grouped' == $product_type ) { - $clear_parent_ids[] = $post_id; - } - - if ( $_POST['previous_parent_id'] > 0 ) { - $clear_parent_ids[] = absint( $_POST['previous_parent_id'] ); - } - - if ( $clear_parent_ids ) { - foreach ( $clear_parent_ids as $clear_id ) { - - $children_by_price = get_posts( array( - 'post_parent' => $clear_id, - 'orderby' => 'meta_value_num', - 'order' => 'asc', - 'meta_key' => '_price', - 'posts_per_page' => 1, - 'post_type' => 'product', - 'fields' => 'ids' - ) ); - - if ( $children_by_price ) { - foreach ( $children_by_price as $child ) { - $child_price = get_post_meta( $child, '_price', true ); - update_post_meta( $clear_id, '_price', $child_price ); - } - } - } - } - } - - // Sold Individually - if ( ! empty( $_POST['_sold_individually'] ) ) { - update_post_meta( $post_id, '_sold_individually', 'yes' ); - } else { - update_post_meta( $post_id, '_sold_individually', '' ); - } - - // Stock Data - if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { - - $manage_stock = 'no'; - $backorders = 'no'; - $stock = ''; - $stock_status = wc_clean( $_POST['_stock_status'] ); - - if ( 'external' === $product_type ) { - - $stock_status = 'instock'; - - } elseif ( 'variable' === $product_type ) { - - // Stock status is always determined by children so sync later - $stock_status = ''; - - if ( ! empty( $_POST['_manage_stock'] ) ) { - $manage_stock = 'yes'; - $backorders = wc_clean( $_POST['_backorders'] ); - } - - } elseif ( 'grouped' !== $product_type && ! empty( $_POST['_manage_stock'] ) ) { - $manage_stock = 'yes'; - $backorders = wc_clean( $_POST['_backorders'] ); - } - - update_post_meta( $post_id, '_manage_stock', $manage_stock ); - update_post_meta( $post_id, '_backorders', $backorders ); - - if ( $stock_status ) { - wc_update_product_stock_status( $post_id, $stock_status ); - } - - if ( ! empty( $_POST['_manage_stock'] ) ) { - wc_update_product_stock( $post_id, wc_stock_amount( $_POST['_stock'] ) ); - } else { - update_post_meta( $post_id, '_stock', '' ); - } - - } else { - wc_update_product_stock_status( $post_id, wc_clean( $_POST['_stock_status'] ) ); - } - - // Upsells - if ( isset( $_POST['upsell_ids'] ) ) { - $upsells = array(); - $ids = $_POST['upsell_ids']; - - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $upsells[] = $id; - } - } - - update_post_meta( $post_id, '_upsell_ids', $upsells ); - } else { - delete_post_meta( $post_id, '_upsell_ids' ); - } - - // Cross sells - if ( isset( $_POST['crosssell_ids'] ) ) { - $crosssells = array(); - $ids = $_POST['crosssell_ids']; - - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $crosssells[] = $id; - } - } - - update_post_meta( $post_id, '_crosssell_ids', $crosssells ); - } else { - delete_post_meta( $post_id, '_crosssell_ids' ); - } - - // Downloadable options - if ( 'yes' == $is_downloadable ) { - - $_download_limit = absint( $_POST['_download_limit'] ); - if ( ! $_download_limit ) { - $_download_limit = ''; // 0 or blank = unlimited - } - - $_download_expiry = absint( $_POST['_download_expiry'] ); - if ( ! $_download_expiry ) { - $_download_expiry = ''; // 0 or blank = unlimited - } - - // file paths will be stored in an array keyed off md5(file path) - $files = array(); - - if ( isset( $_POST['_wc_file_urls'] ) ) { - $file_names = isset( $_POST['_wc_file_names'] ) ? array_map( 'wc_clean', $_POST['_wc_file_names'] ) : array(); - $file_urls = isset( $_POST['_wc_file_urls'] ) ? array_map( 'wc_clean', $_POST['_wc_file_urls'] ) : array(); - $file_url_size = sizeof( $file_urls ); - - for ( $i = 0; $i < $file_url_size; $i ++ ) { - if ( ! empty( $file_urls[ $i ] ) ) { - $files[ md5( $file_urls[ $i ] ) ] = array( - 'name' => $file_names[ $i ], - 'file' => $file_urls[ $i ] - ); - } - } - } - - // grant permission to any newly added files on any existing orders for this product prior to saving - do_action( 'woocommerce_process_product_file_download_paths', $post_id, 0, $files ); - - update_post_meta( $post_id, '_downloadable_files', $files ); - update_post_meta( $post_id, '_download_limit', $_download_limit ); - update_post_meta( $post_id, '_download_expiry', $_download_expiry ); - - if ( isset( $_POST['_download_type'] ) ) { - update_post_meta( $post_id, '_download_type', wc_clean( $_POST['_download_type'] ) ); - } - } - - // Product url - if ( 'external' == $product_type ) { - if ( isset( $_POST['_product_url'] ) ) { - update_post_meta( $post_id, '_product_url', esc_attr( $_POST['_product_url'] ) ); - } - if ( isset( $_POST['_button_text'] ) ) { - update_post_meta( $post_id, '_button_text', esc_attr( $_POST['_button_text'] ) ); - } - } - - // Save variations - if ( 'variable' == $product_type ) { - self::save_variations( $post_id, $post ); - } - - // Do action for product type - do_action( 'woocommerce_process_product_meta_' . $product_type, $post_id ); - - // Clear cache/transients - wc_delete_product_transients( $post_id ); + return $attributes; } /** - * Save meta box data + * Prepare attributes for a specific variation or defaults. + * @param array $all_attributes + * @param string $key_prefix + * @param int $index + * @return array + */ + private static function prepare_set_attributes( $all_attributes, $key_prefix = 'attribute_', $index = null ) { + $attributes = array(); + + if ( $all_attributes ) { + foreach ( $all_attributes as $attribute ) { + if ( $attribute->get_variation() ) { + $attribute_key = sanitize_title( $attribute->get_name() ); + + if ( ! is_null( $index ) ) { + $value = isset( $_POST[ $key_prefix . $attribute_key ][ $index ] ) ? stripslashes( $_POST[ $key_prefix . $attribute_key ][ $index ] ) : ''; + } else { + $value = isset( $_POST[ $key_prefix . $attribute_key ] ) ? stripslashes( $_POST[ $key_prefix . $attribute_key ] ) : ''; + } + + $value = $attribute->is_taxonomy() ? sanitize_title( $value ) : wc_clean( $value ); // Don't use wc_clean as it destroys sanitized characters in terms. + $attributes[ $attribute_key ] = $value; + } + } + } + + return $attributes; + } + + /** + * Save meta box data. + * + * @param int $post_id + * @param $post + */ + public static function save( $post_id, $post ) { + // Process product type first so we have the correct class to run setters. + $product_type = empty( $_POST['product-type'] ) ? WC_Product_Factory::get_product_type( $post_id ) : sanitize_title( stripslashes( $_POST['product-type'] ) ); + $classname = WC_Product_Factory::get_product_classname( $post_id, $product_type ? $product_type : 'simple' ); + $product = new $classname( $post_id ); + $attributes = self::prepare_attributes(); + $errors = $product->set_props( array( + 'sku' => isset( $_POST['_sku'] ) ? wc_clean( $_POST['_sku'] ) : null, + 'purchase_note' => wp_kses_post( stripslashes( $_POST['_purchase_note'] ) ), + 'downloadable' => isset( $_POST['_downloadable'] ), + 'virtual' => isset( $_POST['_virtual'] ), + 'featured' => isset( $_POST['_featured'] ), + 'catalog_visibility' => wc_clean( $_POST['_visibility'] ), + 'tax_status' => isset( $_POST['_tax_status'] ) ? wc_clean( $_POST['_tax_status'] ) : null, + 'tax_class' => isset( $_POST['_tax_class'] ) ? wc_clean( $_POST['_tax_class'] ) : null, + 'weight' => wc_clean( $_POST['_weight'] ), + 'length' => wc_clean( $_POST['_length'] ), + 'width' => wc_clean( $_POST['_width'] ), + 'height' => wc_clean( $_POST['_height'] ), + 'shipping_class_id' => absint( $_POST['product_shipping_class'] ), + 'sold_individually' => ! empty( $_POST['_sold_individually'] ), + 'upsell_ids' => isset( $_POST['upsell_ids'] ) ? array_map( 'intval', (array) $_POST['upsell_ids'] ) : array(), + 'cross_sell_ids' => isset( $_POST['crosssell_ids'] ) ? array_map( 'intval', (array) $_POST['crosssell_ids'] ) : array(), + 'regular_price' => wc_clean( $_POST['_regular_price'] ), + 'sale_price' => wc_clean( $_POST['_sale_price'] ), + 'date_on_sale_from' => wc_clean( $_POST['_sale_price_dates_from'] ), + 'date_on_sale_to' => wc_clean( $_POST['_sale_price_dates_to'] ), + 'manage_stock' => ! empty( $_POST['_manage_stock'] ), + 'backorders' => isset( $_POST['_backorders'] ) ? wc_clean( $_POST['_backorders'] ) : null, + 'stock_status' => wc_clean( $_POST['_stock_status'] ), + 'stock_quantity' => isset( $_POST['_stock'] ) ? wc_stock_amount( $_POST['_stock'] ) : null, + 'download_limit' => '' === $_POST['_download_limit'] ? '' : absint( $_POST['_download_limit'] ), + 'download_expiry' => '' === $_POST['_download_expiry'] ? '' : absint( $_POST['_download_expiry'] ), + 'downloads' => self::prepare_downloads( + isset( $_POST['_wc_file_names'] ) ? $_POST['_wc_file_names'] : array(), + isset( $_POST['_wc_file_urls'] ) ? $_POST['_wc_file_urls'] : array(), + isset( $_POST['_wc_file_hashes'] ) ? $_POST['_wc_file_hashes'] : array() + ), + 'product_url' => esc_url_raw( $_POST['_product_url'] ), + 'button_text' => wc_clean( $_POST['_button_text'] ), + 'children' => 'grouped' === $product_type ? self::prepare_children() : null, + 'reviews_allowed' => ! empty( $_POST['_reviews_allowed'] ), + 'attributes' => $attributes, + 'default_attributes' => self::prepare_set_attributes( $attributes, 'default_attribute_' ), + ) ); + + if ( is_wp_error( $errors ) ) { + WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() ); + } + + /** + * @since 3.0.0 to set props before save. + */ + do_action( 'woocommerce_admin_process_product_object', $product ); + + $product->save(); + + if ( $product->is_type( 'variable' ) ) { + $product->get_data_store()->sync_variation_names( $product, wc_clean( $_POST['original_post_title'] ), wc_clean( $_POST['post_title'] ) ); + } + + do_action( 'woocommerce_process_product_meta_' . $product_type, $post_id ); + } + + /** + * Save meta box data. + * + * @param int $post_id + * @param WP_Post $post */ public static function save_variations( $post_id, $post ) { - global $wpdb; + if ( isset( $_POST['variable_post_id'] ) ) { + $parent = wc_get_product( $post_id ); + $parent->set_default_attributes( self::prepare_set_attributes( $parent->get_attributes(), 'default_attribute_' ) ); + $parent->save(); - $attributes = (array) maybe_unserialize( get_post_meta( $post_id, '_product_attributes', true ) ); - - if ( isset( $_POST['variable_sku'] ) ) { - - $variable_post_id = $_POST['variable_post_id']; - $variable_sku = $_POST['variable_sku']; - $variable_regular_price = $_POST['variable_regular_price']; - $variable_sale_price = $_POST['variable_sale_price']; - $upload_image_id = $_POST['upload_image_id']; - $variable_download_limit = $_POST['variable_download_limit']; - $variable_download_expiry = $_POST['variable_download_expiry']; - $variable_shipping_class = $_POST['variable_shipping_class']; - $variable_tax_class = isset( $_POST['variable_tax_class'] ) ? $_POST['variable_tax_class'] : array(); - $variable_menu_order = $_POST['variation_menu_order']; - $variable_sale_price_dates_from = $_POST['variable_sale_price_dates_from']; - $variable_sale_price_dates_to = $_POST['variable_sale_price_dates_to']; - - $variable_weight = isset( $_POST['variable_weight'] ) ? $_POST['variable_weight'] : array(); - $variable_length = isset( $_POST['variable_length'] ) ? $_POST['variable_length'] : array(); - $variable_width = isset( $_POST['variable_width'] ) ? $_POST['variable_width'] : array(); - $variable_height = isset( $_POST['variable_height'] ) ? $_POST['variable_height'] : array(); - $variable_enabled = isset( $_POST['variable_enabled'] ) ? $_POST['variable_enabled'] : array(); - $variable_is_virtual = isset( $_POST['variable_is_virtual'] ) ? $_POST['variable_is_virtual'] : array(); - $variable_is_downloadable = isset( $_POST['variable_is_downloadable'] ) ? $_POST['variable_is_downloadable'] : array(); - - $variable_manage_stock = isset( $_POST['variable_manage_stock'] ) ? $_POST['variable_manage_stock'] : array(); - $variable_stock = isset( $_POST['variable_stock'] ) ? $_POST['variable_stock'] : array(); - $variable_backorders = isset( $_POST['variable_backorders'] ) ? $_POST['variable_backorders'] : array(); - $variable_stock_status = isset( $_POST['variable_stock_status'] ) ? $_POST['variable_stock_status'] : array(); - - $max_loop = max( array_keys( $_POST['variable_post_id'] ) ); + $max_loop = max( array_keys( $_POST['variable_post_id'] ) ); + $data_store = $parent->get_data_store(); + $data_store->sort_all_product_variations( $parent->get_id() ); for ( $i = 0; $i <= $max_loop; $i ++ ) { - if ( ! isset( $variable_post_id[ $i ] ) ) { + if ( ! isset( $_POST['variable_post_id'][ $i ] ) ) { continue; } + $variation_id = absint( $_POST['variable_post_id'][ $i ] ); + $variation = new WC_Product_Variation( $variation_id ); + $errors = $variation->set_props( array( + 'status' => isset( $_POST['variable_enabled'][ $i ] ) ? 'publish' : 'private', + 'menu_order' => wc_clean( $_POST['variation_menu_order'][ $i ] ), + 'regular_price' => wc_clean( $_POST['variable_regular_price'][ $i ] ), + 'sale_price' => wc_clean( $_POST['variable_sale_price'][ $i ] ), + 'virtual' => isset( $_POST['variable_is_virtual'][ $i ] ), + 'downloadable' => isset( $_POST['variable_is_downloadable'][ $i ] ), + 'date_on_sale_from' => wc_clean( $_POST['variable_sale_price_dates_from'][ $i ] ), + 'date_on_sale_to' => wc_clean( $_POST['variable_sale_price_dates_to'][ $i ] ), + 'description' => wp_kses_post( $_POST['variable_description'][ $i ] ), + 'download_limit' => wc_clean( $_POST['variable_download_limit'][ $i ] ), + 'download_expiry' => wc_clean( $_POST['variable_download_expiry'][ $i ] ), + 'downloads' => self::prepare_downloads( + isset( $_POST['_wc_variation_file_names'][ $variation_id ] ) ? $_POST['_wc_variation_file_names'][ $variation_id ] : array(), + isset( $_POST['_wc_variation_file_urls'][ $variation_id ] ) ? $_POST['_wc_variation_file_urls'][ $variation_id ] : array(), + isset( $_POST['_wc_variation_file_hashes'][ $variation_id ] ) ? $_POST['_wc_variation_file_hashes'][ $variation_id ] : array() + ), + 'manage_stock' => isset( $_POST['variable_manage_stock'][ $i ] ), + 'stock_quantity' => isset( $_POST['variable_stock'], $_POST['variable_stock'][ $i ] ) ? wc_clean( $_POST['variable_stock'][ $i ] ) : null, + 'backorders' => isset( $_POST['variable_backorders'], $_POST['variable_backorders'][ $i ] ) ? wc_clean( $_POST['variable_backorders'][ $i ] ) : null, + 'stock_status' => wc_clean( $_POST['variable_stock_status'][ $i ] ), + 'image_id' => wc_clean( $_POST['upload_image_id'][ $i ] ), + 'attributes' => self::prepare_set_attributes( $parent->get_attributes(), 'attribute_', $i ), + 'sku' => isset( $_POST['variable_sku'][ $i ] ) ? wc_clean( $_POST['variable_sku'][ $i ] ) : '', + 'weight' => isset( $_POST['variable_weight'][ $i ] ) ? wc_clean( $_POST['variable_weight'][ $i ] ) : '', + 'length' => isset( $_POST['variable_length'][ $i ] ) ? wc_clean( $_POST['variable_length'][ $i ] ) : '', + 'width' => isset( $_POST['variable_width'][ $i ] ) ? wc_clean( $_POST['variable_width'][ $i ] ) : '', + 'height' => isset( $_POST['variable_height'][ $i ] ) ? wc_clean( $_POST['variable_height'][ $i ] ) : '', + 'shipping_class_id' => wc_clean( $_POST['variable_shipping_class'][ $i ] ), + 'tax_class' => isset( $_POST['variable_tax_class'][ $i ] ) ? wc_clean( $_POST['variable_tax_class'][ $i ] ) : null, + ) ); - $variation_id = absint( $variable_post_id[ $i ] ); - - // Checkboxes - $is_virtual = isset( $variable_is_virtual[ $i ] ) ? 'yes' : 'no'; - $is_downloadable = isset( $variable_is_downloadable[ $i ] ) ? 'yes' : 'no'; - $post_status = isset( $variable_enabled[ $i ] ) ? 'publish' : 'private'; - $manage_stock = isset( $variable_manage_stock[ $i ] ) ? 'yes' : 'no'; - - // Generate a useful post title - $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), absint( $variation_id ), esc_html( get_the_title( $post_id ) ) ); - - // Update or Add post - if ( ! $variation_id ) { - - $variation = array( - 'post_title' => $variation_post_title, - 'post_content' => '', - 'post_status' => $post_status, - 'post_author' => get_current_user_id(), - 'post_parent' => $post_id, - 'post_type' => 'product_variation', - 'menu_order' => $variable_menu_order[ $i ] - ); - - $variation_id = wp_insert_post( $variation ); - - do_action( 'woocommerce_create_product_variation', $variation_id ); - - } else { - - $wpdb->update( $wpdb->posts, array( 'post_status' => $post_status, 'post_title' => $variation_post_title, 'menu_order' => $variable_menu_order[ $i ] ), array( 'ID' => $variation_id ) ); - - do_action( 'woocommerce_update_product_variation', $variation_id ); - + if ( is_wp_error( $errors ) ) { + WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() ); } - // Only continue if we have a variation ID - if ( ! $variation_id ) { - continue; - } - - // Unique SKU - $sku = get_post_meta( $variation_id, '_sku', true ); - $new_sku = wc_clean( stripslashes( $variable_sku[ $i ] ) ); - - if ( '' == $new_sku ) { - update_post_meta( $variation_id, '_sku', '' ); - } elseif ( $new_sku !== $sku ) { - if ( ! empty( $new_sku ) ) { - $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); - if ( ! $unique_sku ) { - WC_Admin_Meta_Boxes::add_error( __( 'Variation SKU must be unique.', 'woocommerce' ) ); - } else { - update_post_meta( $variation_id, '_sku', $new_sku ); - } - } else { - update_post_meta( $variation_id, '_sku', '' ); - } - } - - // Update post meta - update_post_meta( $variation_id, '_thumbnail_id', absint( $upload_image_id[ $i ] ) ); - update_post_meta( $variation_id, '_virtual', wc_clean( $is_virtual ) ); - update_post_meta( $variation_id, '_downloadable', wc_clean( $is_downloadable ) ); - - if ( isset( $variable_weight[ $i ] ) ) { - update_post_meta( $variation_id, '_weight', ( '' === $variable_weight[ $i ] ) ? '' : wc_format_decimal( $variable_weight[ $i ] ) ); - } - if ( isset( $variable_length[ $i ] ) ) { - update_post_meta( $variation_id, '_length', ( '' === $variable_length[ $i ] ) ? '' : wc_format_decimal( $variable_length[ $i ] ) ); - } - if ( isset( $variable_width[ $i ] ) ) { - update_post_meta( $variation_id, '_width', ( '' === $variable_width[ $i ] ) ? '' : wc_format_decimal( $variable_width[ $i ] ) ); - } - if ( isset( $variable_height[ $i ] ) ) { - update_post_meta( $variation_id, '_height', ( '' === $variable_height[ $i ] ) ? '' : wc_format_decimal( $variable_height[ $i ] ) ); - } - - // Stock handling - update_post_meta( $variation_id, '_manage_stock', $manage_stock ); - - // Only update stock status to user setting if changed by the user, but do so before looking at stock levels at variation level - if ( ! empty( $variable_stock_status[ $i ] ) ) { - wc_update_product_stock_status( $variation_id, $variable_stock_status[ $i ] ); - } - - if ( 'yes' === $manage_stock ) { - if ( isset( $variable_backorders[ $i ] ) && $variable_backorders[ $i ] !== 'parent' ) { - update_post_meta( $variation_id, '_backorders', wc_clean( $variable_backorders[ $i ] ) ); - } else { - delete_post_meta( $variation_id, '_backorders' ); - } - wc_update_product_stock( $variation_id, wc_stock_amount( $variable_stock[ $i ] ) ); - } else { - delete_post_meta( $variation_id, '_backorders' ); - delete_post_meta( $variation_id, '_stock' ); - } - - // Price handling - $regular_price = wc_format_decimal( $variable_regular_price[ $i ] ); - $sale_price = $variable_sale_price[ $i ] === '' ? '' : wc_format_decimal( $variable_sale_price[ $i ] ); - $date_from = wc_clean( $variable_sale_price_dates_from[ $i ] ); - $date_to = wc_clean( $variable_sale_price_dates_to[ $i ] ); - - update_post_meta( $variation_id, '_regular_price', $regular_price ); - update_post_meta( $variation_id, '_sale_price', $sale_price ); - - // Save Dates - update_post_meta( $variation_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' ); - update_post_meta( $variation_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' ); - - if ( $date_to && ! $date_from ) { - update_post_meta( $variation_id, '_sale_price_dates_from', strtotime( 'NOW', current_time( 'timestamp' ) ) ); - } - - // Update price if on sale - if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) { - update_post_meta( $variation_id, '_price', $sale_price ); - } else { - update_post_meta( $variation_id, '_price', $regular_price ); - } - - if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $variation_id, '_price', $sale_price ); - } - - if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $variation_id, '_price', $regular_price ); - update_post_meta( $variation_id, '_sale_price_dates_from', '' ); - update_post_meta( $variation_id, '_sale_price_dates_to', '' ); - } - - if ( isset( $variable_tax_class[ $i ] ) && $variable_tax_class[ $i ] !== 'parent' ) { - update_post_meta( $variation_id, '_tax_class', wc_clean( $variable_tax_class[ $i ] ) ); - } else { - delete_post_meta( $variation_id, '_tax_class' ); - } - - if ( 'yes' == $is_downloadable ) { - update_post_meta( $variation_id, '_download_limit', wc_clean( $variable_download_limit[ $i ] ) ); - update_post_meta( $variation_id, '_download_expiry', wc_clean( $variable_download_expiry[ $i ] ) ); - - $files = array(); - $file_names = isset( $_POST['_wc_variation_file_names'][ $variation_id ] ) ? array_map( 'wc_clean', $_POST['_wc_variation_file_names'][ $variation_id ] ) : array(); - $file_urls = isset( $_POST['_wc_variation_file_urls'][ $variation_id ] ) ? array_map( 'wc_clean', $_POST['_wc_variation_file_urls'][ $variation_id ] ) : array(); - $file_url_size = sizeof( $file_urls ); - - for ( $ii = 0; $ii < $file_url_size; $ii ++ ) { - if ( ! empty( $file_urls[ $ii ] ) ) { - $files[ md5( $file_urls[ $ii ] ) ] = array( - 'name' => $file_names[ $ii ], - 'file' => $file_urls[ $ii ] - ); - } - } - - // grant permission to any newly added files on any existing orders for this product prior to saving - do_action( 'woocommerce_process_product_file_download_paths', $post_id, $variation_id, $files ); - - update_post_meta( $variation_id, '_downloadable_files', $files ); - } else { - update_post_meta( $variation_id, '_download_limit', '' ); - update_post_meta( $variation_id, '_download_expiry', '' ); - update_post_meta( $variation_id, '_downloadable_files', '' ); - } - - // Save shipping class - $variable_shipping_class[ $i ] = ! empty( $variable_shipping_class[ $i ] ) ? (int) $variable_shipping_class[ $i ] : ''; - wp_set_object_terms( $variation_id, $variable_shipping_class[ $i ], 'product_shipping_class'); - - // Update taxonomies - don't use wc_clean as it destroys sanitized characters - $updated_attribute_keys = array(); - foreach ( $attributes as $attribute ) { - if ( $attribute['is_variation'] ) { - $attribute_key = 'attribute_' . sanitize_title( $attribute['name'] ); - $value = isset( $_POST[ $attribute_key ][ $i ] ) ? sanitize_title( stripslashes( $_POST[ $attribute_key ][ $i ] ) ) : ''; - $updated_attribute_keys[] = $attribute_key; - update_post_meta( $variation_id, $attribute_key, $value ); - } - } - - // Remove old taxonomies attributes so data is kept up to date - first get attribute key names - $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); - - foreach ( $delete_attribute_keys as $key ) { - delete_post_meta( $variation_id, $key ); - } + $variation->save(); do_action( 'woocommerce_save_product_variation', $variation_id, $i ); } } - - // Update parent if variable so price sorting works and stays in sync with the cheapest child - WC_Product_Variable::sync( $post_id ); - - // Update default attribute options setting - $default_attributes = array(); - - foreach ( $attributes as $attribute ) { - if ( $attribute['is_variation'] ) { - - // Don't use wc_clean as it destroys sanitized characters - if ( isset( $_POST[ 'default_attribute_' . sanitize_title( $attribute['name'] ) ] ) ) { - $value = sanitize_title( trim( stripslashes( $_POST[ 'default_attribute_' . sanitize_title( $attribute['name'] ) ] ) ) ); - } else { - $value = ''; - } - - if ( $value ) { - $default_attributes[ sanitize_title( $attribute['name'] ) ] = $value; - } - } - } - - update_post_meta( $post_id, '_default_attributes', $default_attributes ); } } diff --git a/includes/admin/meta-boxes/class-wc-meta-box-product-images.php b/includes/admin/meta-boxes/class-wc-meta-box-product-images.php index e07c8044d39..5ac7cca74bd 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-product-images.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-product-images.php @@ -4,21 +4,25 @@ * * Display the product images meta box. * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin/Meta Boxes * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} /** - * WC_Meta_Box_Product_Images + * WC_Meta_Box_Product_Images Class. */ class WC_Meta_Box_Product_Images { /** - * Output the metabox + * Output the metabox. + * + * @param WP_Post $post */ public static function output( $post ) { ?> @@ -34,17 +38,36 @@ class WC_Meta_Box_Product_Images { $product_image_gallery = implode( ',', $attachment_ids ); } - $attachments = array_filter( explode( ',', $product_image_gallery ) ); + $attachments = array_filter( explode( ',', $product_image_gallery ) ); + $update_meta = false; + $updated_gallery_ids = array(); - if ( $attachments ) + if ( ! empty( $attachments ) ) { foreach ( $attachments as $attachment_id ) { + $attachment = wp_get_attachment_image( $attachment_id, 'thumbnail' ); + + // if attachment is empty skip + if ( empty( $attachment ) ) { + $update_meta = true; + continue; + } + echo '
  • - ' . wp_get_attachment_image( $attachment_id, 'thumbnail' ) . ' + ' . $attachment . '
  • '; + + // rebuild ids to be saved + $updated_gallery_ids[] = $attachment_id; } + + // need to update product meta to set new gallery ids + if ( $update_meta ) { + update_post_meta( $post->ID, '_product_image_gallery', implode( ',', $updated_gallery_ids ) ); + } + } ?> @@ -52,17 +75,20 @@ class WC_Meta_Box_Product_Images {

    - +

    comment_ID, 'rating', true ); + ?> + + 'excerpt', - 'quicktags' => array( 'buttons' => 'em,strong,link' ), - 'tinymce' => array( + 'textarea_name' => 'excerpt', + 'quicktags' => array( 'buttons' => 'em,strong,link' ), + 'tinymce' => array( 'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator', 'theme_advanced_buttons2' => '', ), - 'editor_css' => '' + 'editor_css' => '', ); wp_editor( htmlspecialchars_decode( $post->post_excerpt ), 'excerpt', apply_filters( 'woocommerce_product_short_description_editor_settings', $settings ) ); } - } diff --git a/includes/admin/meta-boxes/views/html-order-download-permission.php b/includes/admin/meta-boxes/views/html-order-download-permission.php index 81e86b92195..d4ed1309b7f 100644 --- a/includes/admin/meta-boxes/views/html-order-download-permission.php +++ b/includes/admin/meta-boxes/views/html-order-download-permission.php @@ -1,28 +1,49 @@

    - -
    - - id ) . ' — ' . apply_filters( 'woocommerce_admin_download_permissions_title', $product->get_title(), $download->product_id, $download->order_id, $download->order_key, $download->download_id ) . ' — ' . sprintf( __( '%s: %s', 'woocommerce' ), $file_count, wc_get_filename_from_url( $product->get_file_download_path( $download->download_id ) ) ) . ' — ' . sprintf( _n('Downloaded %s time', 'Downloaded %s times', absint( $download->download_count ), 'woocommerce'), absint( $download->download_count ) ); ?> - + +
    + get_id() ), + esc_html( apply_filters( 'woocommerce_admin_download_permissions_title', $product->get_name(), $download->get_product_id(), $download->get_order_id(), $download->get_order_key(), $download->get_download_id() ) ), + esc_html( $file_count ), + esc_html( wc_get_filename_from_url( $product->get_file_download_path( $download->get_download_id() ) ) ) + ); + printf( _n( 'Downloaded %s time', 'Downloaded %s times', $download->get_download_count(), 'woocommerce' ), esc_html( $download->get_download_count() ) ) + ?>

    +
    - - - - + + + - - + + + + + $download->get_product_id(), + 'order' => $download->get_order_key(), + 'email' => urlencode( $download->get_user_email() ), + 'key' => $download->get_download_id(), + ), trailingslashit( home_url() ) ); + + echo '' . esc_html( $file_count ) . ''; + ?>
    -
    \ No newline at end of file +
    diff --git a/includes/admin/meta-boxes/views/html-order-fee.php b/includes/admin/meta-boxes/views/html-order-fee.php index bf952683047..b04e1fbb7ff 100644 --- a/includes/admin/meta-boxes/views/html-order-fee.php +++ b/includes/admin/meta-boxes/views/html-order-fee.php @@ -1,84 +1,86 @@ - - - - + +
    - + get_name() ? $item->get_name() : __( 'Fee', 'woocommerce' ) ); ?>
    -
    diff --git a/includes/admin/reports/class-wc-admin-report.php b/includes/admin/reports/class-wc-admin-report.php index f9478877924..4b38a333dda 100644 --- a/includes/admin/reports/class-wc-admin-report.php +++ b/includes/admin/reports/class-wc-admin-report.php @@ -1,23 +1,61 @@ array( - * 'type' => 'meta', - * 'function' => 'SUM', - * 'name' => 'total_sales' + * 'type' => 'meta', + * 'function' => 'SUM', + * 'name' => 'total_sales' * ) * * @param array $args - * @return array|string depending on query_type + * @return mixed depending on query_type */ public function get_order_report_data( $args = array() ) { global $wpdb; - $defaults = array( - 'data' => array(), - 'where' => array(), - 'where_meta' => array(), - 'query_type' => 'get_row', - 'group_by' => '', - 'order_by' => '', - 'limit' => '', - 'filter_range' => false, - 'nocache' => false, - 'debug' => false + $default_args = array( + 'data' => array(), + 'where' => array(), + 'where_meta' => array(), + 'query_type' => 'get_row', + 'group_by' => '', + 'order_by' => '', + 'limit' => '', + 'filter_range' => false, + 'nocache' => false, + 'debug' => false, + 'order_types' => wc_get_order_types( 'reports' ), + 'order_status' => array( 'completed', 'processing', 'on-hold' ), + 'parent_order_status' => false, ); - - $args = apply_filters( 'woocommerce_reports_get_order_report_data_args', wp_parse_args( $args, $defaults ) ); + $args = apply_filters( 'woocommerce_reports_get_order_report_data_args', $args ); + $args = wp_parse_args( $args, $default_args ); extract( $args ); if ( empty( $data ) ) { - return false; + return ''; } + $order_status = apply_filters( 'woocommerce_reports_order_statuses', $order_status ); + + $query = array(); $select = array(); foreach ( $data as $key => $value ) { $distinct = ''; - if ( isset( $value['distinct'] ) ) + if ( isset( $value['distinct'] ) ) { $distinct = 'DISTINCT'; + } - if ( $value['type'] == 'meta' ) - $get_key = "meta_{$key}.meta_value"; - elseif( $value['type'] == 'post_data' ) - $get_key = "posts.{$key}"; - elseif( $value['type'] == 'order_item_meta' ) - $get_key = "order_item_meta_{$key}.meta_value"; - elseif( $value['type'] == 'order_item' ) - $get_key = "order_items.{$key}"; + switch ( $value['type'] ) { + case 'meta' : + $get_key = "meta_{$key}.meta_value"; + break; + case 'parent_meta' : + $get_key = "parent_meta_{$key}.meta_value"; + break; + case 'post_data' : + $get_key = "posts.{$key}"; + break; + case 'order_item_meta' : + $get_key = "order_item_meta_{$key}.meta_value"; + break; + case 'order_item' : + $get_key = "order_items.{$key}"; + break; + default : + continue; + } - if ( $value['function'] ) + if ( $value['function'] ) { $get = "{$value['function']}({$distinct} {$get_key})"; - else + } else { $get = "{$distinct} {$get_key}"; + } $select[] = "{$get} as {$value['name']}"; } @@ -87,107 +144,136 @@ class WC_Admin_Report { $query['from'] = "FROM {$wpdb->posts} AS posts"; // Joins - $joins = array(); + $joins = array(); - foreach ( $data as $key => $value ) { - if ( $value['type'] == 'meta' ) { + foreach ( ( $data + $where ) as $key => $value ) { + $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER'; + $type = isset( $value['type'] ) ? $value['type'] : false; - $joins["meta_{$key}"] = "LEFT JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id"; + switch ( $type ) { + case 'meta' : + $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON ( posts.ID = meta_{$key}.post_id AND meta_{$key}.meta_key = '{$key}' )"; + break; + case 'parent_meta' : + $joins[ "parent_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS parent_meta_{$key} ON (posts.post_parent = parent_meta_{$key}.post_id) AND (parent_meta_{$key}.meta_key = '{$key}')"; + break; + case 'order_item_meta' : + $joins["order_items"] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON (posts.ID = order_items.order_id)"; - } elseif ( $value['type'] == 'order_item_meta' ) { - - $joins["order_items"] = "LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; - $joins["order_item_meta_{$key}"] = "LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON order_items.order_item_id = order_item_meta_{$key}.order_item_id"; - - } elseif ( $value['type'] == 'order_item' ) { - - $joins["order_items"] = "LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; + if ( ! empty( $value['order_item_type'] ) ) { + $joins["order_items"] .= " AND (order_items.order_item_type = '{$value['order_item_type']}')"; + } + $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON " . + "(order_items.order_item_id = order_item_meta_{$key}.order_item_id) " . + " AND (order_item_meta_{$key}.meta_key = '{$key}')"; + break; + case 'order_item' : + $joins["order_items"] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; + break; } } if ( ! empty( $where_meta ) ) { foreach ( $where_meta as $value ) { - if ( ! is_array( $value ) ) + if ( ! is_array( $value ) ) { continue; + } + $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER'; + $type = isset( $value['type'] ) ? $value['type'] : false; + $key = is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key']; - $key = is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key']; + if ( 'order_item_meta' === $type ) { - if ( isset( $value['type'] ) && $value['type'] == 'order_item_meta' ) { - - $joins["order_items"] = "LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; - $joins["order_item_meta_{$key}"] = "LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON order_items.order_item_id = order_item_meta_{$key}.order_item_id"; + $joins["order_items"] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; + $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON order_items.order_item_id = order_item_meta_{$key}.order_item_id"; } else { // If we have a where clause for meta, join the postmeta table - $joins["meta_{$key}"] = "LEFT JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id"; + $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id"; } } } + if ( ! empty( $parent_order_status ) ) { + $joins["parent"] = "LEFT JOIN {$wpdb->posts} AS parent ON posts.post_parent = parent.ID"; + } + $query['join'] = implode( ' ', $joins ); $query['where'] = " - WHERE posts.post_type = 'shop_order' - AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "') + WHERE posts.post_type IN ( '" . implode( "','", $order_types ) . "' ) "; - if ( $filter_range ) { + if ( ! empty( $order_status ) ) { $query['where'] .= " - AND post_date >= '" . date('Y-m-d', $this->start_date ) . "' - AND post_date < '" . date('Y-m-d', strtotime( '+1 DAY', $this->end_date ) ) . "' + AND posts.post_status IN ( 'wc-" . implode( "','wc-", $order_status ) . "') "; } - foreach ( $data as $key => $value ) { - if ( $value['type'] == 'meta' ) { - - $query['where'] .= " AND meta_{$key}.meta_key = '{$key}'"; - - } elseif ( $value['type'] == 'order_item_meta' ) { - - $query['where'] .= " AND order_items.order_item_type = '{$value['order_item_type']}'"; - $query['where'] .= " AND order_item_meta_{$key}.meta_key = '{$key}'"; - + if ( ! empty( $parent_order_status ) ) { + if ( ! empty( $order_status ) ) { + $query['where'] .= " AND ( parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') OR parent.ID IS NULL ) "; + } else { + $query['where'] .= " AND parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') "; } } + if ( $filter_range ) { + $query['where'] .= " + AND posts.post_date >= '" . date( 'Y-m-d H:i:s', $this->start_date ) . "' + AND posts.post_date < '" . date( 'Y-m-d H:i:s', strtotime( '+1 DAY', $this->end_date ) ) . "' + "; + } + if ( ! empty( $where_meta ) ) { + $relation = isset( $where_meta['relation'] ) ? $where_meta['relation'] : 'AND'; $query['where'] .= " AND ("; foreach ( $where_meta as $index => $value ) { - if ( ! is_array( $value ) ) + + if ( ! is_array( $value ) ) { continue; + } $key = is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key']; - if ( strtolower( $value['operator'] ) == 'in' ) { - if ( is_array( $value['meta_value'] ) ) + if ( strtolower( $value['operator'] ) == 'in' || strtolower( $value['operator'] ) == 'not in' ) { + + if ( is_array( $value['meta_value'] ) ) { $value['meta_value'] = implode( "','", $value['meta_value'] ); - if ( ! empty( $value['meta_value'] ) ) - $where_value = "IN ('{$value['meta_value']}')"; + } + + if ( ! empty( $value['meta_value'] ) ) { + $where_value = "{$value['operator']} ('{$value['meta_value']}')"; + } } else { $where_value = "{$value['operator']} '{$value['meta_value']}'"; } if ( ! empty( $where_value ) ) { - if ( $index > 0 ) + if ( $index > 0 ) { $query['where'] .= ' ' . $relation; + } - if ( isset( $value['type'] ) && $value['type'] == 'order_item_meta' ) { - if ( is_array( $value['meta_key'] ) ) + if ( isset( $value['type'] ) && 'order_item_meta' === $value['type'] ) { + + if ( is_array( $value['meta_key'] ) ) { $query['where'] .= " ( order_item_meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')"; - else + } else { $query['where'] .= " ( order_item_meta_{$key}.meta_key = '{$value['meta_key']}'"; + } $query['where'] .= " AND order_item_meta_{$key}.meta_value {$where_value} )"; } else { - if ( is_array( $value['meta_key'] ) ) + + if ( is_array( $value['meta_key'] ) ) { $query['where'] .= " ( meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')"; - else + } else { $query['where'] .= " ( meta_{$key}.meta_key = '{$value['meta_key']}'"; + } $query['where'] .= " AND meta_{$key}.meta_value {$where_value} )"; } @@ -198,18 +284,25 @@ class WC_Admin_Report { } if ( ! empty( $where ) ) { + foreach ( $where as $value ) { - if ( strtolower( $value['operator'] ) == 'in' ) { - if ( is_array( $value['value'] ) ) + + if ( strtolower( $value['operator'] ) == 'in' || strtolower( $value['operator'] ) == 'not in' ) { + + if ( is_array( $value['value'] ) ) { $value['value'] = implode( "','", $value['value'] ); - if ( ! empty( $value['value'] ) ) - $where_value = "IN ('{$value['value']}')"; + } + + if ( ! empty( $value['value'] ) ) { + $where_value = "{$value['operator']} ('{$value['value']}')"; + } } else { $where_value = "{$value['operator']} '{$value['value']}'"; } - if ( ! empty( $where_value ) ) + if ( ! empty( $where_value ) ) { $query['where'] .= " AND {$value['key']} {$where_value}"; + } } } @@ -231,10 +324,14 @@ class WC_Admin_Report { $cached_results = get_transient( strtolower( get_class( $this ) ) ); if ( $debug ) { - var_dump( $query ); + echo '
    ';
    +			wc_print_r( $query );
    +			echo '
    '; } if ( $debug || $nocache || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { + // Enable big selects for reports + $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $cached_results[ $query_hash ] = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data ); set_transient( strtolower( get_class( $this ) ), $cached_results, DAY_IN_SECONDS ); } @@ -245,7 +342,7 @@ class WC_Admin_Report { } /** - * Put data with post_date's into an array of times + * Put data with post_date's into an array of times. * * @param array $data array of your data * @param string $date_key key for the 'date' field. e.g. 'post_date' @@ -253,25 +350,38 @@ class WC_Admin_Report { * @param int $interval * @param string $start_date * @param string $group_by - * @return string + * @return array */ public function prepare_chart_data( $data, $date_key, $data_key, $interval, $start_date, $group_by ) { $prepared_data = array(); - $time = ''; - - // Ensure all days (or months) have values first in this range - for ( $i = 0; $i <= $interval; $i ++ ) { - switch ( $group_by ) { - case 'day' : - $time = strtotime( date( 'Ymd', strtotime( "+{$i} DAY", $start_date ) ) ) . '000'; - break; - case 'month' : - $time = strtotime( date( 'Ym', strtotime( "+{$i} MONTH", $start_date ) ) . '01' ) . '000'; - break; + + // Ensure all days (or months) have values in this range. + if ( 'day' === $group_by ) { + for ( $i = 0; $i <= $interval; $i ++ ) { + $time = strtotime( date( 'Ymd', strtotime( "+{$i} DAY", $start_date ) ) ) . '000'; + + if ( ! isset( $prepared_data[ $time ] ) ) { + $prepared_data[ $time ] = array( esc_js( $time ), 0 ); + } + } + } else { + $current_yearnum = date( 'Y', $start_date ); + $current_monthnum = date( 'm', $start_date ); + + for ( $i = 0; $i <= $interval; $i ++ ) { + $time = strtotime( $current_yearnum . str_pad( $current_monthnum, 2, '0', STR_PAD_LEFT ) . '01' ) . '000'; + + if ( ! isset( $prepared_data[ $time ] ) ) { + $prepared_data[ $time ] = array( esc_js( $time ), 0 ); + } + + $current_monthnum ++; + + if ( $current_monthnum > 12 ) { + $current_monthnum = 1; + $current_yearnum ++; + } } - - if ( ! isset( $prepared_data[ $time ] ) ) - $prepared_data[ $time ] = array( esc_js( $time ), 0 ); } foreach ( $data as $d ) { @@ -280,24 +390,27 @@ class WC_Admin_Report { $time = strtotime( date( 'Ymd', strtotime( $d->$date_key ) ) ) . '000'; break; case 'month' : + default : $time = strtotime( date( 'Ym', strtotime( $d->$date_key ) ) . '01' ) . '000'; break; } - if ( ! isset( $prepared_data[ $time ] ) ) + if ( ! isset( $prepared_data[ $time ] ) ) { continue; + } - if ( $data_key ) + if ( $data_key ) { $prepared_data[ $time ][1] += $d->$data_key; - else + } else { $prepared_data[ $time ][1] ++; + } } return $prepared_data; } /** - * Prepares a sparkline to show sales in the last X days + * Prepares a sparkline to show sales in the last X days. * * @param int $id ID of the product to show. Blank to get all orders. * @param int $days Days of stats to get. @@ -307,7 +420,7 @@ class WC_Admin_Report { public function sales_sparkline( $id = '', $days = 7, $type = 'sales' ) { if ( $id ) { - $meta_key = $type == 'sales' ? '_line_total' : '_qty'; + $meta_key = ( 'sales' === $type ) ? '_line_total' : '_qty'; $data = $this->get_order_report_data( array( 'data' => array( @@ -315,153 +428,200 @@ class WC_Admin_Report { 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', - 'name' => 'product_id' + 'name' => 'product_id', ), $meta_key => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', - 'name' => 'sparkline_value' + 'name' => 'sparkline_value', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'post_date', 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ), - 'operator' => '>' + 'operator' => '>', ), array( 'key' => 'order_item_meta__product_id.meta_value', 'value' => $id, - 'operator' => '=' - ) + 'operator' => '=', + ), ), - 'group_by' => 'YEAR(post_date), MONTH(post_date), DAY(post_date)', + 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)', 'query_type' => 'get_results', - 'filter_range' => false + 'filter_range' => false, ) ); } else { + $data = $this->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', - 'name' => 'sparkline_value' + 'name' => 'sparkline_value', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'post_date', 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ), - 'operator' => '>' - ) + 'operator' => '>', + ), ), - 'group_by' => 'YEAR(post_date), MONTH(post_date), DAY(post_date)', + 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)', 'query_type' => 'get_results', - 'filter_range' => false + 'filter_range' => false, ) ); } $total = 0; - foreach ( $data as $d ) + foreach ( $data as $d ) { $total += $d->sparkline_value; + } - if ( $type == 'sales' ) { - $tooltip = sprintf( __( 'Sold %s worth in the last %d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days ); + if ( 'sales' === $type ) { + /* translators: 1: total income 2: days */ + $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days ); } else { - $tooltip = sprintf( _n( 'Sold 1 item in the last %d days', 'Sold %d items in the last %d days', $total, 'woocommerce' ), $total, $days ); + /* translators: 1: total items sold 2: days */ + $tooltip = sprintf( _n( 'Sold 1 item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days ); } $sparkline_data = array_values( $this->prepare_chart_data( $data, 'post_date', 'sparkline_value', $days - 1, strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ), 'day' ) ); - return ''; + return ''; } /** - * Get the current range and calculate the start and end dates + * Get the current range and calculate the start and end dates. * * @param string $current_range */ public function calculate_current_range( $current_range ) { - switch ( $current_range ) { - case 'custom' : - $this->start_date = strtotime( sanitize_text_field( $_GET['start_date'] ) ); - $this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( $_GET['end_date'] ) ) ); - if ( ! $this->end_date ) - $this->end_date = current_time('timestamp'); + switch ( $current_range ) { + + case 'custom' : + + $this->start_date = max( strtotime( '-20 years' ), strtotime( sanitize_text_field( $_GET['start_date'] ) ) ); + + if ( empty( $_GET['end_date'] ) ) { + $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); + } else { + $this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( $_GET['end_date'] ) ) ); + } $interval = 0; $min_date = $this->start_date; + while ( ( $min_date = strtotime( "+1 MONTH", $min_date ) ) <= $this->end_date ) { - $interval ++; + $interval ++; } // 3 months max for day view - if ( $interval > 3 ) - $this->chart_groupby = 'month'; - else - $this->chart_groupby = 'day'; + if ( $interval > 3 ) { + $this->chart_groupby = 'month'; + } else { + $this->chart_groupby = 'day'; + } break; + case 'year' : - $this->start_date = strtotime( date( 'Y-01-01', current_time('timestamp') ) ); + $this->start_date = strtotime( date( 'Y-01-01', current_time( 'timestamp' ) ) ); $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); $this->chart_groupby = 'month'; break; + case 'last_month' : - $this->start_date = strtotime( date( 'Y-m-01', strtotime( '-1 MONTH', current_time('timestamp') ) ) ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( '-1 MONTH', current_time('timestamp') ) ) ); - $this->chart_groupby = 'day'; + $first_day_current_month = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) ); + $this->start_date = strtotime( date( 'Y-m-01', strtotime( '-1 DAY', $first_day_current_month ) ) ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( '-1 DAY', $first_day_current_month ) ) ); + $this->chart_groupby = 'day'; break; + case 'month' : - $this->start_date = strtotime( date( 'Y-m-01', current_time('timestamp') ) ); - $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); - $this->chart_groupby = 'day'; + $this->start_date = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) ); + $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); + $this->chart_groupby = 'day'; break; + case '7day' : - $this->start_date = strtotime( '-6 days', current_time( 'timestamp' ) ); - $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); - $this->chart_groupby = 'day'; + $this->start_date = strtotime( '-6 days', strtotime( 'midnight', current_time( 'timestamp' ) ) ); + $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); + $this->chart_groupby = 'day'; break; } // Group by switch ( $this->chart_groupby ) { + case 'day' : - $this->group_by_query = 'YEAR(post_date), MONTH(post_date), DAY(post_date)'; - $this->chart_interval = ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) ); - $this->barwidth = 60 * 60 * 24 * 1000; + $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; + $this->chart_interval = absint( ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) ) ); + $this->barwidth = 60 * 60 * 24 * 1000; break; + case 'month' : - $this->group_by_query = 'YEAR(post_date), MONTH(post_date)'; + $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date)'; $this->chart_interval = 0; - $min_date = $this->start_date; - while ( ( $min_date = strtotime( "+1 MONTH", $min_date ) ) <= $this->end_date ) { + $min_date = strtotime( date( 'Y-m-01', $this->start_date ) ); + + while ( ( $min_date = strtotime( "+1 MONTH", $min_date ) ) <= $this->end_date ) { $this->chart_interval ++; } - $this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000; + + $this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000; break; } } /** - * Get the main chart + * Return currency tooltip JS based on WooCommerce currency position settings. + * + * @return string + */ + public function get_currency_tooltip() { + switch ( get_option( 'woocommerce_currency_pos' ) ) { + case 'right': + $currency_tooltip = 'append_tooltip: "' . get_woocommerce_currency_symbol() . '"'; + break; + case 'right_space': + $currency_tooltip = 'append_tooltip: " ' . get_woocommerce_currency_symbol() . '"'; + break; + case 'left': + $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '"'; + break; + case 'left_space': + default: + $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . ' "'; + break; + } + + return $currency_tooltip; + } + + /** + * Get the main chart. + * * @return string */ public function get_main_chart() {} /** - * Get the legend for the main chart sidebar + * Get the legend for the main chart sidebar. + * * @return array */ public function get_chart_legend() { @@ -469,7 +629,8 @@ class WC_Admin_Report { } /** - * [get_chart_widgets description] + * Get chart widgets. + * * @return array */ public function get_chart_widgets() { @@ -477,12 +638,29 @@ class WC_Admin_Report { } /** - * Get an export link if needed + * Get an export link if needed. */ public function get_export_button() {} /** - * Output the report + * Output the report. */ public function output_report() {} + + /** + * Check nonce for current range. + * + * @since 3.0.4 + * @param string $current_range Current range. + */ + public function check_current_range_nonce( $current_range ) { + if ( 'custom' !== $current_range ) { + return; + } + + if ( ! isset( $_GET['wc_reports_nonce'] ) || ! wp_verify_nonce( $_GET['wc_reports_nonce'], 'custom_range' ) ) { + wp_safe_redirect( remove_query_arg( array( 'start_date', 'end_date', 'range', 'wc_reports_nonce' ) ) ); + exit; + } + } } diff --git a/includes/admin/reports/class-wc-report-coupon-usage.php b/includes/admin/reports/class-wc-report-coupon-usage.php index ff5dbc17587..2df70e25fb8 100644 --- a/includes/admin/reports/class-wc-report-coupon-usage.php +++ b/includes/admin/reports/class-wc-report-coupon-usage.php @@ -1,19 +1,35 @@ get_order_report_data( array( + $total_discount_query = array( 'data' => array( 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', - 'name' => 'discount_amount' - ) + 'name' => 'discount_amount', + ), ), 'where' => array( array( - 'type' => 'order_item', - 'key' => 'order_item_name', - 'value' => $this->coupon_codes, - 'operator' => 'IN' - ) + 'key' => 'order_item_type', + 'value' => 'coupon', + 'operator' => '=', + ), ), 'query_type' => 'get_var', - 'filter_range' => true - ) ); + 'filter_range' => true, + 'order_types' => wc_get_order_types( 'order-count' ), + ); - $total_coupons = absint( $this->get_order_report_data( array( + $total_coupons_query = array( 'data' => array( - 'order_item_name' => array( + 'order_item_id' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', - 'name' => 'order_coupon_count' - ) + 'name' => 'order_coupon_count', + ), ), 'where' => array( array( - 'type' => 'order_item', - 'key' => 'order_item_name', - 'value' => $this->coupon_codes, - 'operator' => 'IN' - ) + 'key' => 'order_item_type', + 'value' => 'coupon', + 'operator' => '=', + ), ), 'query_type' => 'get_var', - 'filter_range' => true - ) ) ); + 'filter_range' => true, + 'order_types' => wc_get_order_types( 'order-count' ), + ); + + if ( ! empty( $this->coupon_codes ) ) { + $coupon_code_query = array( + 'type' => 'order_item', + 'key' => 'order_item_name', + 'value' => $this->coupon_codes, + 'operator' => 'IN', + ); + + $total_discount_query['where'][] = $coupon_code_query; + $total_coupons_query['where'][] = $coupon_code_query; + } + + $total_discount = $this->get_order_report_data( $total_discount_query ); + $total_coupons = absint( $this->get_order_report_data( $total_coupons_query ) ); $legend[] = array( + /* translators: %s: discount amount */ 'title' => sprintf( __( '%s discounts in total', 'woocommerce' ), '' . wc_price( $total_discount ) . '' ), 'color' => $this->chart_colours['discount_amount'], - 'highlight_series' => 1 + 'highlight_series' => 1, ); $legend[] = array( + /* translators: %s: coupons amount */ 'title' => sprintf( __( '%s coupons used in total', 'woocommerce' ), '' . $total_coupons . '' ), - 'color' => $this->chart_colours['coupon_count' ], - 'highlight_series' => 0 + 'color' => $this->chart_colours['coupon_count'], + 'highlight_series' => 0, ); return $legend; } /** - * Output the report + * Output the report. */ public function output_report() { + $ranges = array( 'year' => __( 'Year', 'woocommerce' ), - 'last_month' => __( 'Last Month', 'woocommerce' ), - 'month' => __( 'This Month', 'woocommerce' ), - '7day' => __( 'Last 7 Days', 'woocommerce' ) + 'last_month' => __( 'Last month', 'woocommerce' ), + 'month' => __( 'This month', 'woocommerce' ), + '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( @@ -103,19 +138,21 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { 'coupon_count' => '#d4d9dc', ); - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : '7day'; + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = '7day'; } + $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); - include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'); + include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php' ); } /** - * [get_chart_widgets description] + * Get chart widgets. + * * @return array */ public function get_chart_widgets() { @@ -123,15 +160,14 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { $widgets[] = array( 'title' => '', - 'callback' => array( $this, 'coupons_widget' ) + 'callback' => array( $this, 'coupons_widget' ), ); return $widgets; } /** - * Product selection - * @return void + * Output coupons widget. */ public function coupons_widget() { ?> @@ -147,23 +183,23 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { 'order_item_type' => 'coupon', 'function' => '', 'distinct' => true, - 'name' => 'order_item_name' - ) + 'name' => 'order_item_name', + ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', - 'operator' => '=' - ) + 'operator' => '=', + ), ), 'query_type' => 'get_col', - 'filter_range' => false + 'filter_range' => false, ) ); - if ( $used_coupons ) : + if ( ! empty( $used_coupons ) && is_array( $used_coupons ) ) : ?> - - - - - - - - - + + + + + + +
    -

    +

    'order_item', 'order_item_type' => 'coupon', 'function' => '', - 'name' => 'coupon_code' + 'name' => 'coupon_code', ), 'order_item_id' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', - 'name' => 'coupon_count' + 'name' => 'coupon_count', ), ), 'where' => array( @@ -213,21 +244,21 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { 'type' => 'order_item', 'key' => 'order_item_type', 'value' => 'coupon', - 'operator' => '=' - ) + 'operator' => '=', + ), ), 'order_by' => 'coupon_count DESC', 'group_by' => 'order_item_name', 'limit' => 12, 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); - if ( $most_popular ) { + if ( ! empty( $most_popular ) && is_array( $most_popular ) ) { foreach ( $most_popular as $coupon ) { echo ' - + '; } } else { @@ -236,7 +267,7 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { ?>
    ' . $coupon->coupon_count . '' . $coupon->coupon_code . '' . $coupon->coupon_code . '
    -

    +

    'order_item', 'order_item_type' => 'coupon', 'function' => '', - 'name' => 'coupon_code' + 'name' => 'coupon_code', ), 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', - 'name' => 'discount_amount' - ) + 'name' => 'discount_amount', + ), ), 'where' => array( array( 'type' => 'order_item', 'key' => 'order_item_type', 'value' => 'coupon', - 'operator' => '=' - ) + 'operator' => '=', + ), ), 'order_by' => 'discount_amount DESC', 'group_by' => 'order_item_name', 'limit' => 12, 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); - if ( $most_discount ) { + if ( ! empty( $most_discount ) && is_array( $most_discount ) ) { foreach ( $most_discount as $coupon ) { echo ' - + '; } } else { @@ -308,17 +339,17 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { } /** - * Output an export link + * Output an export link. */ public function get_export_button() { - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : '7day'; + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day'; ?> @@ -327,77 +358,93 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { } /** - * Get the main chart + * Get the main chart. + * * @return string */ public function get_main_chart() { global $wp_locale; // Get orders and dates in range - we want the SUM of order totals, COUNT of order items, COUNT of orders, and the date - $order_coupon_counts = $this->get_order_report_data( array( + $order_coupon_counts_query = array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', - 'name' => 'order_coupon_count' + 'name' => 'order_coupon_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), ), 'where' => array( array( - 'type' => 'order_item', - 'key' => 'order_item_name', - 'value' => $this->coupon_codes, - 'operator' => 'IN' - ) + 'key' => 'order_item_type', + 'value' => 'coupon', + 'operator' => '=', + ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', - 'filter_range' => true - ) ); + 'filter_range' => true, + 'order_types' => wc_get_order_types( 'order-count' ), + ); - $order_discount_amounts = $this->get_order_report_data( array( + $order_discount_amounts_query = array( 'data' => array( 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', - 'name' => 'discount_amount' + 'name' => 'discount_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), ), 'where' => array( array( - 'type' => 'order_item', - 'key' => 'order_item_name', - 'value' => $this->coupon_codes, - 'operator' => 'IN' - ) + 'key' => 'order_item_type', + 'value' => 'coupon', + 'operator' => '=', + ), ), 'group_by' => $this->group_by_query . ', order_item_name', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', - 'filter_range' => true - ) ); + 'filter_range' => true, + 'order_types' => wc_get_order_types( 'order-count' ), + ); + + if ( ! empty( $this->coupon_codes ) ) { + $coupon_code_query = array( + 'type' => 'order_item', + 'key' => 'order_item_name', + 'value' => $this->coupon_codes, + 'operator' => 'IN', + ); + + $order_coupon_counts_query['where'][] = $coupon_code_query; + $order_discount_amounts_query['where'][] = $coupon_code_query; + } + + $order_coupon_counts = $this->get_order_report_data( $order_coupon_counts_query ); + $order_discount_amounts = $this->get_order_report_data( $order_discount_amounts_query ); // Prepare data for report - $order_coupon_counts = $this->prepare_chart_data( $order_coupon_counts, 'post_date', 'order_coupon_count' , $this->chart_interval, $this->start_date, $this->chart_groupby ); + $order_coupon_counts = $this->prepare_chart_data( $order_coupon_counts, 'post_date', 'order_coupon_count' , $this->chart_interval, $this->start_date, $this->chart_groupby ); $order_discount_amounts = $this->prepare_chart_data( $order_discount_amounts, 'post_date', 'discount_amount', $this->chart_interval, $this->start_date, $this->chart_groupby ); // Encode in json format $chart_data = json_encode( array( - 'order_coupon_counts' => array_values( $order_coupon_counts ), - 'order_discount_amounts' => array_values( $order_discount_amounts ) + 'order_coupon_counts' => array_values( $order_coupon_counts ), + 'order_discount_amounts' => array_values( $order_discount_amounts ), ) ); ?>
    @@ -414,8 +461,8 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { { label: "", data: order_data.order_coupon_counts, - color: 'chart_colours['coupon_count' ]; ?>', - bars: { fillColor: 'chart_colours['coupon_count' ]; ?>', fill: true, show: true, lineWidth: 0, barWidth: barwidth; ?> * 0.5, align: 'center' }, + color: 'chart_colours['coupon_count']; ?>', + bars: { fillColor: 'chart_colours['coupon_count']; ?>', fill: true, show: true, lineWidth: 0, barWidth: barwidth; ?> * 0.5, align: 'center' }, shadowSize: 0, hoverable: false }, @@ -427,7 +474,7 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 4, fill: false }, shadowSize: 0, - prepend_tooltip: "" + get_currency_tooltip(); ?> } ]; @@ -462,7 +509,7 @@ class WC_Report_Coupon_Usage extends WC_Admin_Report { position: "bottom", tickColor: 'transparent', mode: "time", - timeformat: "chart_groupby == 'day' ) echo '%d %b'; else echo '%b'; ?>", + timeformat: "chart_groupby ) ? '%d %b' : '%b'; ?>", monthNames: month_abbrev ) ) ?>, tickLength: 1, minTickSize: [1, "chart_groupby; ?>"], diff --git a/includes/admin/reports/class-wc-report-customer-list.php b/includes/admin/reports/class-wc-report-customer-list.php index 7636fa8ea8e..e01bedfe5e2 100644 --- a/includes/admin/reports/class-wc-report-customer-list.php +++ b/includes/admin/reports/class-wc-report-customer-list.php @@ -1,4 +1,5 @@ __( 'Customer', 'woocommerce' ), - 'plural' => __( 'Customers', 'woocommerce' ), - 'ajax' => false + 'singular' => 'customer', + 'plural' => 'customers', + 'ajax' => false, ) ); } /** - * No items found text + * No items found text. */ public function no_items() { _e( 'No customers found.', 'woocommerce' ); } /** - * Output the report + * Output the report. */ public function output_report() { $this->prepare_items(); @@ -51,6 +51,16 @@ class WC_Report_Customer_List extends WP_List_Table { echo '

    ' . sprintf( _n( '%s previous order linked', '%s previous orders linked', $linked, 'woocommerce' ), $linked ) . '

    '; } + if ( ! empty( $_GET['refresh'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'refresh' ) ) { + $user_id = absint( $_GET['refresh'] ); + $user = get_user_by( 'id', $user_id ); + + delete_user_meta( $user_id, '_money_spent' ); + delete_user_meta( $user_id, '_order_count' ); + + echo '

    ' . sprintf( __( 'Refreshed stats for %s', 'woocommerce' ), $user->display_name ) . '

    '; + } + echo '
    '; $this->search_box( __( 'Search customers', 'woocommerce' ), 'customer_search' ); @@ -61,32 +71,31 @@ class WC_Report_Customer_List extends WP_List_Table { } /** - * column_default function. - * @access public - * @param mixed $user + * Get column value. + * + * @param WP_User $user * @param string $column_name - * @return int|string - * @todo Inconsistent return types, and void return at the end. Needs a rewrite. + * @return string */ - function column_default( $user, $column_name ) { - global $wpdb; + public function column_default( $user, $column_name ) { + switch ( $column_name ) { - switch( $column_name ) { case 'customer_name' : if ( $user->last_name && $user->first_name ) { return $user->last_name . ', ' . $user->first_name; } else { return '-'; } + case 'username' : return $user->user_login; - break; + case 'location' : $state_code = get_user_meta( $user->ID, 'billing_state', true ); $country_code = get_user_meta( $user->ID, 'billing_country', true ); - $state = isset( WC()->countries->states[ $country_code ][ $state_code ] ) ? WC()->countries->states[ $country_code ][ $state_code ] : $state_code; + $state = isset( WC()->countries->states[ $country_code ][ $state_code ] ) ? WC()->countries->states[ $country_code ][ $state_code ] : $state_code; $country = isset( WC()->countries->countries[ $country_code ] ) ? WC()->countries->countries[ $country_code ] : $country_code; $value = ''; @@ -102,115 +111,70 @@ class WC_Report_Customer_List extends WP_List_Table { } else { return '-'; } - break; + case 'email' : return '
    ' . $user->user_email . ''; + case 'spent' : - if ( ! $spent = get_user_meta( $user->ID, '_money_spent', true ) ) { + return wc_price( wc_get_customer_total_spent( $user->ID ) ); - $spent = $wpdb->get_var( "SELECT SUM(meta2.meta_value) - FROM $wpdb->posts as posts - - LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id - LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id - - WHERE meta.meta_key = '_customer_user' - AND meta.meta_value = $user->ID - AND posts.post_type = 'shop_order' - AND posts.post_status = 'wc-completed' - AND meta2.meta_key = '_order_total' - " ); - - update_user_meta( $user->ID, '_money_spent', $spent ); - } - - return wc_price( $spent ); - break; case 'orders' : - if ( ! $count = get_user_meta( $user->ID, '_order_count', true ) ) { + return wc_get_customer_order_count( $user->ID ); - $count = $wpdb->get_var( "SELECT COUNT(*) - FROM $wpdb->posts as posts - - LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id - - WHERE meta.meta_key = '_customer_user' - AND posts.post_type = 'shop_order' - AND posts.post_status = 'wc-completed' - AND meta_value = $user->ID - " ); - - update_user_meta( $user->ID, '_order_count', $count ); - } - - return absint( $count ); - break; case 'last_order' : - $order_ids = get_posts( array( - 'posts_per_page' => 1, - 'post_type' => 'shop_order', - 'orderby' => 'date', - 'order' => 'desc', - 'post_status' => array_keys( wc_get_order_statuses() ), - 'meta_query' => array( - array( - 'key' => '_customer_user', - 'value' => $user->ID - ) - ), - 'fields' => 'ids' + $orders = wc_get_orders( array( + 'limit' => 1, + 'status' => array_map( 'wc_get_order_status_name', wc_get_is_paid_statuses() ), + 'customer' => $user->ID, ) ); - if ( $order_ids ) { - $order = get_order( $order_ids[0] ); - - echo '' . $order->get_order_number() . ' – ' . date_i18n( get_option( 'date_format' ), strtotime( $order->order_date ) ); - } else echo '-'; + if ( ! empty( $orders ) ) { + $order = $orders[0]; + return '' . _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number() . ' – ' . wc_format_datetime( $order->get_date_created() ); + } else { + return '-'; + } break; + case 'user_actions' : + ob_start(); ?>

    wp_nonce_url( add_query_arg( 'refresh', $user->ID ), 'refresh' ), + 'name' => __( 'Refresh stats', 'woocommerce' ), + 'action' => "refresh", + ); + $actions['edit'] = array( - 'url' => admin_url( 'user-edit.php?user_id=' . $user->ID ), - 'name' => __( 'Edit', 'woocommerce' ), - 'action' => "edit" + 'url' => admin_url( 'user-edit.php?user_id=' . $user->ID ), + 'name' => __( 'Edit', 'woocommerce' ), + 'action' => "edit", ); $actions['view'] = array( - 'url' => admin_url( 'edit.php?post_type=shop_order&_customer_user=' . $user->ID ), - 'name' => __( 'View orders', 'woocommerce' ), - 'action' => "view" + 'url' => admin_url( 'edit.php?post_type=shop_order&_customer_user=' . $user->ID ), + 'name' => __( 'View orders', 'woocommerce' ), + 'action' => "view", ); - $order_ids = get_posts( array( - 'posts_per_page' => 1, - 'post_type' => 'shop_order', - 'post_status' => array_keys( wc_get_order_statuses() ), - 'meta_query' => array( - array( - 'key' => '_customer_user', - 'value' => array( 0, '' ), - 'compare' => 'IN' - ), - array( - 'key' => '_billing_email', - 'value' => $user->user_email - ) - ), - 'fields' => 'ids' + $orders = wc_get_orders( array( + 'limit' => 1, + 'status' => array_map( 'wc_get_order_status_name', wc_get_is_paid_statuses() ), + 'customer' => array( array( 0, $user->user_email ) ), ) ); - if ( $order_ids ) { + if ( $orders ) { $actions['link'] = array( - 'url' => wp_nonce_url( add_query_arg( 'link_orders', $user->ID ), 'link_orders' ), - 'name' => __( 'Link previous orders', 'woocommerce' ), - 'action' => "link" + 'url' => wp_nonce_url( add_query_arg( 'link_orders', $user->ID ), 'link_orders' ), + 'name' => __( 'Link previous orders', 'woocommerce' ), + 'action' => "link", ); } @@ -223,32 +187,41 @@ class WC_Report_Customer_List extends WP_List_Table { do_action( 'woocommerce_admin_user_actions_end', $user ); ?>

    __( 'Name (Last, First)', 'woocommerce' ), 'username' => __( 'Username', 'woocommerce' ), 'email' => __( 'Email', 'woocommerce' ), 'location' => __( 'Location', 'woocommerce' ), 'orders' => __( 'Orders', 'woocommerce' ), - 'spent' => __( 'Spent', 'woocommerce' ), + 'spent' => __( 'Money spent', 'woocommerce' ), 'last_order' => __( 'Last order', 'woocommerce' ), - 'user_actions' => __( 'Actions', 'woocommerce' ) + 'user_actions' => __( 'Actions', 'woocommerce' ), ); return $columns; } /** - * Order users by name + * Order users by name. + * + * @param WP_User_Query $query + * + * @return WP_User_Query */ public function order_by_last_name( $query ) { global $wpdb; @@ -269,44 +242,40 @@ class WC_Report_Customer_List extends WP_List_Table { } /** - * prepare_items function. - * - * @access public + * Prepare customer list items. */ public function prepare_items() { - global $wpdb; - $current_page = absint( $this->get_pagenum() ); $per_page = 20; /** - * Init column headers + * Init column headers. */ $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); add_action( 'pre_user_query', array( $this, 'order_by_last_name' ) ); /** - * Get users + * Get users. */ $admin_users = new WP_User_Query( array( - 'role' => 'administrator', - 'fields' => 'ID' + 'role' => 'administrator1', + 'fields' => 'ID', ) ); $manager_users = new WP_User_Query( array( 'role' => 'shop_manager', - 'fields' => 'ID' + 'fields' => 'ID', ) ); $query = new WP_User_Query( array( 'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ), 'number' => $per_page, - 'offset' => ( $current_page - 1 ) * $per_page + 'offset' => ( $current_page - 1 ) * $per_page, ) ); $this->items = $query->get_results(); @@ -314,12 +283,12 @@ class WC_Report_Customer_List extends WP_List_Table { remove_action( 'pre_user_query', array( $this, 'order_by_last_name' ) ); /** - * Pagination + * Pagination. */ $this->set_pagination_args( array( 'total_items' => $query->total_users, 'per_page' => $per_page, - 'total_pages' => ceil( $query->total_users / $per_page ) + 'total_pages' => ceil( $query->total_users / $per_page ), ) ); } } diff --git a/includes/admin/reports/class-wc-report-customers.php b/includes/admin/reports/class-wc-report-customers.php index 8c635441094..2386e9dd326 100644 --- a/includes/admin/reports/class-wc-report-customers.php +++ b/includes/admin/reports/class-wc-report-customers.php @@ -1,34 +1,54 @@ sprintf( __( '%s signups in this period', 'woocommerce' ), '' . sizeof( $this->customers ) . '' ), 'color' => $this->chart_colours['signups'], - 'highlight_series' => 2 + 'highlight_series' => 2, ); return $legend; } /** - * [get_chart_widgets description] + * Get chart widgets. + * * @return array */ public function get_chart_widgets() { @@ -36,15 +56,14 @@ class WC_Report_Customers extends WC_Admin_Report { $widgets[] = array( 'title' => '', - 'callback' => array( $this, 'customers_vs_guests' ) + 'callback' => array( $this, 'customers_vs_guests' ), ); return $widgets; } /** - * customers_vs_guests - * @return void + * Output customers vs guests chart. */ public function customers_vs_guests() { @@ -53,17 +72,17 @@ class WC_Report_Customers extends WC_Admin_Report { 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', - 'name' => 'total_orders' - ) + 'name' => 'total_orders', + ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', - 'operator' => '>' - ) + 'operator' => '>', + ), ), - 'filter_range' => true + 'filter_range' => true, ) ); $guest_order_totals = $this->get_order_report_data( array( @@ -71,24 +90,24 @@ class WC_Report_Customers extends WC_Admin_Report { 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', - 'name' => 'total_orders' - ) + 'name' => 'total_orders', + ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', - 'operator' => '=' - ) + 'operator' => '=', + ), ), - 'filter_range' => true + 'filter_range' => true, ) ); ?>
      -
    • -
    • +
    • +
    __( 'Year', 'woocommerce' ), - 'last_month' => __( 'Last Month', 'woocommerce' ), - 'month' => __( 'This Month', 'woocommerce' ), - '7day' => __( 'Last 7 Days', 'woocommerce' ) + 'last_month' => __( 'Last month', 'woocommerce' ), + 'month' => __( 'This month', 'woocommerce' ), + '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'signups' => '#3498db', 'customers' => '#1abc9c', - 'guests' => '#8fdece' + 'guests' => '#8fdece', ); - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : '7day'; + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day'; - if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) + if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = '7day'; + } + $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $admin_users = new WP_User_Query( array( 'role' => 'administrator', - 'fields' => 'ID' + 'fields' => 'ID', ) ); $manager_users = new WP_User_Query( array( 'role' => 'shop_manager', - 'fields' => 'ID' + 'fields' => 'ID', ) ); $users_query = new WP_User_Query( array( 'fields' => array( 'user_registered' ), - 'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ) + 'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ), ) ); $this->customers = $users_query->get_results(); foreach ( $this->customers as $key => $customer ) { - if ( strtotime( $customer->user_registered ) < $this->start_date || strtotime( $customer->user_registered ) > $this->end_date ) + if ( strtotime( $customer->user_registered ) < $this->start_date || strtotime( $customer->user_registered ) > $this->end_date ) { unset( $this->customers[ $key ] ); + } } include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php' ); } /** - * Output an export link + * Output an export link. */ public function get_export_button() { - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : '7day'; + + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day'; ?> @@ -210,8 +234,7 @@ class WC_Report_Customers extends WC_Admin_Report { } /** - * Get the main chart - * @return string + * Output the main chart. */ public function get_main_chart() { global $wp_locale; @@ -221,25 +244,25 @@ class WC_Report_Customers extends WC_Admin_Report { 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', - 'name' => 'total_orders' + 'name' => 'total_orders', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', - 'operator' => '>' - ) + 'operator' => '>', + ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); $guest_orders = $this->get_order_report_data( array( @@ -247,25 +270,25 @@ class WC_Report_Customers extends WC_Admin_Report { 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', - 'name' => 'total_orders' + 'name' => 'total_orders', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', - 'operator' => '=' - ) + 'operator' => '=', + ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); $signups = $this->prepare_chart_data( $this->customers, 'user_registered', '', $this->chart_interval, $this->start_date, $this->chart_groupby ); @@ -276,7 +299,7 @@ class WC_Report_Customers extends WC_Admin_Report { $chart_data = json_encode( array( 'signups' => array_values( $signups ), 'customer_orders' => array_values( $customer_orders ), - 'guest_orders' => array_values( $guest_orders ) + 'guest_orders' => array_values( $guest_orders ), ) ); ?>
    @@ -291,7 +314,7 @@ class WC_Report_Customers extends WC_Admin_Report { var drawGraph = function( highlight ) { var series = [ { - label: "", + label: "", data: chart_data.customer_orders, color: 'chart_colours['customers']; ?>', bars: { fillColor: 'chart_colours['customers']; ?>', fill: true, show: true, lineWidth: 0, barWidth: barwidth; ?> * 0.5, align: 'center' }, @@ -301,7 +324,7 @@ class WC_Report_Customers extends WC_Admin_Report { stack: true, }, { - label: "", + label: "", data: chart_data.guest_orders, color: 'chart_colours['guests']; ?>', bars: { fillColor: 'chart_colours['guests']; ?>', fill: true, show: true, lineWidth: 0, barWidth: barwidth; ?> * 0.5, align: 'center' }, @@ -343,38 +366,38 @@ class WC_Report_Customers extends WC_Admin_Report { legend: { show: false }, - grid: { - color: '#aaa', - borderColor: 'transparent', - borderWidth: 0, - hoverable: true - }, - xaxes: [ { - color: '#aaa', - position: "bottom", - tickColor: 'transparent', + grid: { + color: '#aaa', + borderColor: 'transparent', + borderWidth: 0, + hoverable: true + }, + xaxes: [ { + color: '#aaa', + position: "bottom", + tickColor: 'transparent', mode: "time", - timeformat: "chart_groupby == 'day' ) echo '%d %b'; else echo '%b'; ?>", + timeformat: "chart_groupby ) ? '%d %b' : '%b'; ?>", monthNames: month_abbrev ) ) ?>, tickLength: 1, minTickSize: [1, "chart_groupby; ?>"], tickSize: [1, "chart_groupby; ?>"], font: { - color: "#aaa" - } + color: "#aaa" + } } ], - yaxes: [ - { - min: 0, - minTickSize: 1, - tickDecimals: 0, - color: '#ecf0f1', - font: { color: "#aaa" } - } - ], - } - ); - jQuery('.chart-placeholder').resize(); + yaxes: [ + { + min: 0, + minTickSize: 1, + tickDecimals: 0, + color: '#ecf0f1', + font: { color: "#aaa" } + } + ], + } + ); + jQuery('.chart-placeholder').resize(); } drawGraph(); @@ -391,4 +414,4 @@ class WC_Report_Customers extends WC_Admin_Report { max_items = 0; $this->items = array(); @@ -37,21 +41,18 @@ class WC_Report_Low_In_Stock extends WC_Report_Stock { $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); $nostock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); - $query_from = "FROM {$wpdb->posts} as posts + $query_from = apply_filters( 'woocommerce_report_low_in_stock_query_from', "FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id WHERE 1=1 - AND posts.post_type IN ('product', 'product_variation') - AND posts.post_status = 'publish' - AND ( - postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$stock}' AND CAST(postmeta.meta_value AS SIGNED) > '{$nostock}' AND postmeta.meta_value != '' - ) - AND ( - ( postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' ) OR ( posts.post_type = 'product_variation' ) - ) - "; + AND posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status = 'publish' + AND postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' + AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$stock}' + AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) > '{$nostock}' + " ); $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT posts.ID as id, posts.post_parent as parent {$query_from} GROUP BY posts.ID ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); $this->max_items = $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ); - } -} \ No newline at end of file + } +} diff --git a/includes/admin/reports/class-wc-report-most-stocked.php b/includes/admin/reports/class-wc-report-most-stocked.php index d361333845e..5ca160e2d5b 100644 --- a/includes/admin/reports/class-wc-report-most-stocked.php +++ b/includes/admin/reports/class-wc-report-most-stocked.php @@ -1,12 +1,15 @@ max_items = 0; - $this->items = array(); + $this->max_items = 0; + $this->items = array(); - // Get products using a query - this is too advanced for get_posts :( - $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 0 ) ); + // Get products using a query - this is too advanced for get_posts :( + $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 0 ) ); - $query_from = "FROM {$wpdb->posts} as posts - INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id - INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id - WHERE 1=1 - AND posts.post_type IN ('product', 'product_variation') - AND posts.post_status = 'publish' - AND ( - postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) > '{$stock}' AND postmeta.meta_value != '' - ) - AND ( - ( postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' ) OR ( posts.post_type = 'product_variation' ) - ) - "; + $query_from = "FROM {$wpdb->posts} as posts + INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id + INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id + WHERE 1=1 + AND posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status = 'publish' + AND postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' + AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) > '{$stock}' + "; - $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT posts.ID as id, posts.post_parent as parent {$query_from} GROUP BY posts.ID ORDER BY CAST(postmeta.meta_value AS SIGNED) DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); - $this->max_items = $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ); - } -} \ No newline at end of file + $query_from = apply_filters( 'woocommerce_report_most_stocked_query_from', $query_from ); + + $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT posts.ID as id, posts.post_parent as parent {$query_from} GROUP BY posts.ID ORDER BY CAST(postmeta.meta_value AS SIGNED) DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); + $this->max_items = $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ); + } +} diff --git a/includes/admin/reports/class-wc-report-out-of-stock.php b/includes/admin/reports/class-wc-report-out-of-stock.php index 5489106f820..0205e954f37 100644 --- a/includes/admin/reports/class-wc-report-out-of-stock.php +++ b/includes/admin/reports/class-wc-report-out-of-stock.php @@ -1,12 +1,15 @@ max_items = 0; - $this->items = array(); + $this->max_items = 0; + $this->items = array(); - // Get products using a query - this is too advanced for get_posts :( - $stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); + // Get products using a query - this is too advanced for get_posts :( + $stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); - $query_from = "FROM {$wpdb->posts} as posts - INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id - INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id - WHERE 1=1 - AND posts.post_type IN ('product', 'product_variation') - AND posts.post_status = 'publish' - AND ( - postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$stock}' AND postmeta.meta_value != '' - ) - AND ( - ( postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' ) OR ( posts.post_type = 'product_variation' ) - ) - "; + $query_from = apply_filters( 'woocommerce_report_out_of_stock_query_from', "FROM {$wpdb->posts} as posts + INNER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id + INNER JOIN {$wpdb->postmeta} AS postmeta2 ON posts.ID = postmeta2.post_id + WHERE 1=1 + AND posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status = 'publish' + AND postmeta2.meta_key = '_manage_stock' AND postmeta2.meta_value = 'yes' + AND postmeta.meta_key = '_stock' AND CAST(postmeta.meta_value AS SIGNED) <= '{$stock}' + " ); - $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT posts.ID as id, posts.post_parent as parent {$query_from} GROUP BY posts.ID ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); - $this->max_items = $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ); - } -} \ No newline at end of file + $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT posts.ID as id, posts.post_parent as parent {$query_from} GROUP BY posts.ID ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); + $this->max_items = $wpdb->get_var( "SELECT COUNT( DISTINCT posts.ID ) {$query_from};" ); + } +} diff --git a/includes/admin/reports/class-wc-report-sales-by-category.php b/includes/admin/reports/class-wc-report-sales-by-category.php index e0166bc020d..390a6b22210 100644 --- a/includes/admin/reports/class-wc-report-sales-by-category.php +++ b/includes/admin/reports/class-wc-report-sales-by-category.php @@ -1,30 +1,59 @@ show_categories = array_map( 'absint', $_GET['show_categories'] ); - } elseif ( isset( $_GET['show_categories'] ) ) { - $this->show_categories = array( absint( $_GET['show_categories'] ) ); + if ( isset( $_GET['show_categories'] ) ) { + $this->show_categories = is_array( $_GET['show_categories'] ) ? array_map( 'absint', $_GET['show_categories'] ) : array( absint( $_GET['show_categories'] ) ); } } /** - * Get all product ids in a category (and its children) + * Get all product ids in a category (and its children). + * * @param int $category_id * @return array */ @@ -37,11 +66,13 @@ class WC_Report_Sales_By_Category extends WC_Admin_Report { } /** - * Get the legend for the main chart sidebar + * Get the legend for the main chart sidebar. + * * @return array */ public function get_chart_legend() { - if ( ! $this->show_categories ) { + + if ( empty( $this->show_categories ) ) { return array(); } @@ -49,20 +80,23 @@ class WC_Report_Sales_By_Category extends WC_Admin_Report { $index = 0; foreach ( $this->show_categories as $category ) { + $category = get_term( $category, 'product_cat' ); $total = 0; $product_ids = $this->get_products_in_category( $category->term_id ); foreach ( $product_ids as $id ) { + if ( isset( $this->item_sales[ $id ] ) ) { $total += $this->item_sales[ $id ]; } } $legend[] = array( - 'title' => sprintf( __( '%s sales in %s', 'woocommerce' ), '' . wc_price( $total ) . '', $category->name ), - 'color' => isset( $this->chart_colours[ $index ] ) ? $this->chart_colours[ $index ] : $this->chart_colours[ 0 ], - 'highlight_series' => $index + /* translators: 1: total items sold 2: category name */ + 'title' => sprintf( __( '%1$s sales in %2$s', 'woocommerce' ), '' . wc_price( $total ) . '', $category->name ), + 'color' => isset( $this->chart_colours[ $index ] ) ? $this->chart_colours[ $index ] : $this->chart_colours[0], + 'highlight_series' => $index, ); $index++; @@ -72,63 +106,68 @@ class WC_Report_Sales_By_Category extends WC_Admin_Report { } /** - * Output the report + * Output the report. */ public function output_report() { + $ranges = array( 'year' => __( 'Year', 'woocommerce' ), - 'last_month' => __( 'Last Month', 'woocommerce' ), - 'month' => __( 'This Month', 'woocommerce' ), - '7day' => __( 'Last 7 Days', 'woocommerce' ) + 'last_month' => __( 'Last month', 'woocommerce' ), + 'month' => __( 'This month', 'woocommerce' ), + '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( '#3498db', '#34495e', '#1abc9c', '#2ecc71', '#f1c40f', '#e67e22', '#e74c3c', '#2980b9', '#8e44ad', '#2c3e50', '#16a085', '#27ae60', '#f39c12', '#d35400', '#c0392b' ); - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : '7day'; + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = '7day'; } + $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); // Get item sales data - if ( $this->show_categories ) { + if ( ! empty( $this->show_categories ) ) { $order_items = $this->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', - 'name' => 'product_id' + 'name' => 'product_id', ), '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', - 'function' => '', - 'name' => 'order_item_amount' + 'function' => 'SUM', + 'name' => 'order_item_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), ), - 'group_by' => 'ID, product_id', + 'group_by' => 'ID, product_id, post_date', 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); - $this->item_sales = array(); + $this->item_sales = array(); $this->item_sales_and_times = array(); - if ( $order_items ) { + if ( is_array( $order_items ) ) { + foreach ( $order_items as $order_item ) { + switch ( $this->chart_groupby ) { case 'day' : $time = strtotime( date( 'Ymd', strtotime( $order_item->post_date ) ) ) * 1000; break; case 'month' : + default : $time = strtotime( date( 'Ym', strtotime( $order_item->post_date ) ) . '01' ) * 1000; break; } @@ -144,28 +183,30 @@ class WC_Report_Sales_By_Category extends WC_Admin_Report { } /** - * [get_chart_widgets description] + * Get chart widgets. + * * @return array */ public function get_chart_widgets() { + return array( array( 'title' => __( 'Categories', 'woocommerce' ), - 'callback' => array( $this, 'category_widget' ) - ) + 'callback' => array( $this, 'category_widget' ), + ), ); } /** - * Category selection - * @return void + * Output category widget. */ public function category_widget() { + $categories = get_terms( 'product_cat', array( 'orderby' => 'name' ) ); ?>
    - - - - - - - - + + + + + + +
    product_ids = array_map( 'absint', $_GET['product_ids'] ); - elseif ( isset( $_GET['product_ids'] ) ) - $this->product_ids = array( absint( $_GET['product_ids'] ) ); + if ( isset( $_GET['product_ids'] ) && is_array( $_GET['product_ids'] ) ) { + $this->product_ids = array_filter( array_map( 'absint', $_GET['product_ids'] ) ); + } elseif ( isset( $_GET['product_ids'] ) ) { + $this->product_ids = array_filter( array( absint( $_GET['product_ids'] ) ) ); + } } /** - * Get the legend for the main chart sidebar + * Get the legend for the main chart sidebar. * @return array */ public function get_chart_legend() { - if ( ! $this->product_ids ) + + if ( empty( $this->product_ids ) ) { return array(); + } $legend = array(); - $total_sales = $this->get_order_report_data( array( + $total_sales = $this->get_order_report_data( array( 'data' => array( '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', - 'name' => 'order_item_amount' - ) + 'name' => 'order_item_amount', + ), ), 'where_meta' => array( 'relation' => 'OR', @@ -47,20 +73,21 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), 'meta_value' => $this->product_ids, - 'operator' => 'IN' + 'operator' => 'IN', ) ), 'query_type' => 'get_var', - 'filter_range' => true + 'filter_range' => true, ) ); - $total_items = absint( $this->get_order_report_data( array( + + $total_items = absint( $this->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', - 'name' => 'order_item_count' - ) + 'name' => 'order_item_count', + ), ), 'where_meta' => array( 'relation' => 'OR', @@ -68,36 +95,40 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), 'meta_value' => $this->product_ids, - 'operator' => 'IN' + 'operator' => 'IN', ) ), 'query_type' => 'get_var', - 'filter_range' => true + 'filter_range' => true, ) ) ); $legend[] = array( + /* translators: %s: total items sold */ 'title' => sprintf( __( '%s sales for the selected items', 'woocommerce' ), '' . wc_price( $total_sales ) . '' ), 'color' => $this->chart_colours['sales_amount'], - 'highlight_series' => 1 + 'highlight_series' => 1, ); + $legend[] = array( - 'title' => sprintf( __( '%s purchases for the selected items', 'woocommerce' ), '' . $total_items . '' ), + /* translators: %s: total items purchased */ + 'title' => sprintf( __( '%s purchases for the selected items', 'woocommerce' ), '' . ( $total_items ) . '' ), 'color' => $this->chart_colours['item_count'], - 'highlight_series' => 0 + 'highlight_series' => 0, ); return $legend; } /** - * Output the report + * Output the report. */ public function output_report() { + $ranges = array( 'year' => __( 'Year', 'woocommerce' ), - 'last_month' => __( 'Last Month', 'woocommerce' ), - 'month' => __( 'This Month', 'woocommerce' ), - '7day' => __( 'Last 7 Days', 'woocommerce' ) + 'last_month' => __( 'Last month', 'woocommerce' ), + 'month' => __( 'This month', 'woocommerce' ), + '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( @@ -105,18 +136,21 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { 'item_count' => '#d4d9dc', ); - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : '7day'; + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day'; - if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) + if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = '7day'; + } + $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); - include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'); + include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php' ); } /** - * [get_chart_widgets description] + * Get chart widgets. + * * @return array */ public function get_chart_widgets() { @@ -126,27 +160,29 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { if ( ! empty( $this->product_ids ) ) { $widgets[] = array( 'title' => __( 'Showing reports for:', 'woocommerce' ), - 'callback' => array( $this, 'current_filters' ) + 'callback' => array( $this, 'current_filters' ), ); } $widgets[] = array( 'title' => '', - 'callback' => array( $this, 'products_widget' ) + 'callback' => array( $this, 'products_widget' ), ); return $widgets; } /** - * Show current filters - * @return void + * Output current filters. */ public function current_filters() { + $this->product_ids_titles = array(); foreach ( $this->product_ids as $product_id ) { - $product = get_product( $product_id ); + + $product = wc_get_product( $product_id ); + if ( $product ) { $this->product_ids_titles[] = $product->get_formatted_name(); } else { @@ -154,54 +190,32 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { } } - echo '

    ' . ' ' . implode( ', ', $this->product_ids_titles ) . '

    '; - echo '

    ' . __( 'Reset', 'woocommerce' ) . '

    '; + echo '

    ' . ' ' . esc_html( implode( ', ', $this->product_ids_titles ) ) . '

    '; + echo '

    ' . __( 'Reset', 'woocommerce' ) . '

    '; } /** - * Product selection - * @return void + * Output products widget. */ public function products_widget() { ?> -

    +

    - - - - - - - - + + + + + + + + +
    -
    -

    +

    ' . wc_price( $coupon->discount_amount ) . '' . $coupon->coupon_code . '' . $coupon->coupon_code . '
    'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', - 'name' => 'product_id' + 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', - 'name' => 'order_item_qty' - ) + 'name' => 'order_item_qty', + ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => 12, 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); if ( $top_sellers ) { foreach ( $top_sellers as $product ) { echo ' - + '; } @@ -241,7 +255,55 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { ?>
    ' . $product->order_item_qty . '' . get_the_title( $product->product_id ) . '' . get_the_title( $product->product_id ) . ' ' . $this->sales_sparkline( $product->product_id, 7, 'count' ) . '
    -

    +

    +
    + + get_order_report_data( array( + 'data' => array( + '_product_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => '', + 'name' => 'product_id', + ), + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ), + ), + 'where_meta' => array( + array( + 'type' => 'order_item_meta', + 'meta_key' => '_line_subtotal', + 'meta_value' => '0', + 'operator' => '=', + ), + ), + 'order_by' => 'order_item_qty DESC', + 'group_by' => 'product_id', + 'limit' => 12, + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + if ( $top_freebies ) { + foreach ( $top_freebies as $product ) { + echo ' + + + + '; + } + } else { + echo ''; + } + ?> +
    ' . $product->order_item_qty . '' . get_the_title( $product->product_id ) . '' . $this->sales_sparkline( $product->product_id, 7, 'count' ) . '
    ' . __( 'No products found in range', 'woocommerce' ) . '
    +
    +

    'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', - 'name' => 'product_id' + 'name' => 'product_id', ), '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', - 'name' => 'order_item_total' - ) + 'name' => 'order_item_total', + ), ), 'order_by' => 'order_item_total DESC', 'group_by' => 'product_id', 'limit' => 12, 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); if ( $top_earners ) { foreach ( $top_earners as $product ) { echo ' - + '; } @@ -304,17 +366,18 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { } /** - * Output an export link + * Output an export link. */ public function get_export_button() { - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : '7day'; + + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day'; ?> @@ -323,16 +386,17 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { } /** - * Get the main chart + * Get the main chart. + * * @return string */ public function get_main_chart() { global $wp_locale; - if ( ! $this->product_ids ) { + if ( empty( $this->product_ids ) ) { ?>
    -

    +

    'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', - 'name' => 'order_item_count' + 'name' => 'order_item_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', - 'name' => 'product_id' - ) + 'name' => 'product_id', + ), ), 'where_meta' => array( 'relation' => 'OR', @@ -363,13 +427,13 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), 'meta_value' => $this->product_ids, - 'operator' => 'IN' + 'operator' => 'IN', ), ), 'group_by' => 'product_id,' . $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); $order_item_amounts = $this->get_order_report_data( array( @@ -378,18 +442,18 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', - 'name' => 'order_item_amount' + 'name' => 'order_item_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', - 'name' => 'post_date' + 'name' => 'post_date', ), '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', - 'name' => 'product_id' + 'name' => 'product_id', ), ), 'where_meta' => array( @@ -398,13 +462,13 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), 'meta_value' => $this->product_ids, - 'operator' => 'IN' + 'operator' => 'IN', ), ), 'group_by' => 'product_id, ' . $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, ) ); // Prepare data for report @@ -414,7 +478,7 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { // Encode in json format $chart_data = json_encode( array( 'order_item_counts' => array_values( $order_item_counts ), - 'order_item_amounts' => array_values( $order_item_amounts ) + 'order_item_amounts' => array_values( $order_item_amounts ), ) ); ?>
    @@ -445,7 +509,7 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true }, lines: { show: true, lineWidth: 4, fill: false }, shadowSize: 0, - prepend_tooltip: "" + get_currency_tooltip(); ?> } ]; @@ -469,47 +533,47 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { legend: { show: false }, - grid: { - color: '#aaa', - borderColor: 'transparent', - borderWidth: 0, - hoverable: true - }, - xaxes: [ { - color: '#aaa', - position: "bottom", - tickColor: 'transparent', + grid: { + color: '#aaa', + borderColor: 'transparent', + borderWidth: 0, + hoverable: true + }, + xaxes: [ { + color: '#aaa', + position: "bottom", + tickColor: 'transparent', mode: "time", - timeformat: "chart_groupby == 'day' ) echo '%d %b'; else echo '%b'; ?>", + timeformat: "chart_groupby ) ? '%d %b' : '%b'; ?>", monthNames: month_abbrev ) ) ?>, tickLength: 1, minTickSize: [1, "chart_groupby; ?>"], font: { - color: "#aaa" - } + color: "#aaa" + } } ], - yaxes: [ - { - min: 0, - minTickSize: 1, - tickDecimals: 0, - color: '#ecf0f1', - font: { color: "#aaa" } - }, - { - position: "right", - min: 0, - tickDecimals: 2, - alignTicksWithAxis: 1, - color: 'transparent', - font: { color: "#aaa" } - } - ], - } - ); + yaxes: [ + { + min: 0, + minTickSize: 1, + tickDecimals: 0, + color: '#ecf0f1', + font: { color: "#aaa" } + }, + { + position: "right", + min: 0, + tickDecimals: 2, + alignTicksWithAxis: 1, + color: 'transparent', + font: { color: "#aaa" } + } + ], + } + ); - jQuery('.chart-placeholder').resize(); - } + jQuery('.chart-placeholder').resize(); + } drawGraph(); @@ -526,4 +590,4 @@ class WC_Report_Sales_By_Product extends WC_Admin_Report { __( 'Stock', 'woocommerce' ), - 'plural' => __( 'Stock', 'woocommerce' ), - 'ajax' => false - ) ); - } - - /** - * No items found text - */ - public function no_items() { - _e( 'No products found.', 'woocommerce' ); - } - - /** - * Don't need this - */ - public function display_tablenav( $position ) { - if ( $position != 'top' ) - parent::display_tablenav( $position ); - } + /** + * Max items. + * + * @var int + */ + protected $max_items; /** - * Output the report + * Constructor. + */ + public function __construct() { + + parent::__construct( array( + 'singular' => 'stock', + 'plural' => 'stock', + 'ajax' => false, + ) ); + } + + /** + * No items found text. + */ + public function no_items() { + _e( 'No products found.', 'woocommerce' ); + } + + /** + * Don't need this. + * + * @param string $position + */ + public function display_tablenav( $position ) { + + if ( 'top' !== $position ) { + parent::display_tablenav( $position ); + } + } + + /** + * Output the report. */ public function output_report() { + $this->prepare_items(); echo '
    '; $this->display(); echo '
    '; } - /** - * column_default function. - * - * @access public - * @param mixed $item - * @param mixed $column_name - */ - function column_default( $item, $column_name ) { - global $product; + /** + * Get column value. + * + * @param mixed $item + * @param string $column_name + */ + public function column_default( $item, $column_name ) { + global $product; - if ( ! $product || $product->id !== $item->id ) - $product = get_product( $item->id ); + if ( ! $product || $product->get_id() !== $item->id ) { + $product = wc_get_product( $item->id ); + } - switch( $column_name ) { - case 'product' : - if ( $sku = $product->get_sku() ) - echo $sku . ' - '; + if ( ! $product ) { + return; + } - echo $product->get_title(); + switch ( $column_name ) { - // Get variation data - if ( $product->is_type( 'variation' ) ) { - $list_attributes = array(); - $attributes = $product->get_variation_attributes(); + case 'product' : + if ( $sku = $product->get_sku() ) { + echo esc_html( $sku ) . ' - '; + } - foreach ( $attributes as $name => $attribute ) { - $list_attributes[] = wc_attribute_label( str_replace( 'attribute_', '', $name ) ) . ': ' . $attribute . ''; - } + echo esc_html( $product->get_name() ); - echo '
    ' . implode( ', ', $list_attributes ) . '
    '; - } - break; - case 'parent' : - if ( $item->parent ) - echo get_the_title( $item->parent ); - else - echo '-'; - break; - case 'stock_status' : - if ( $product->is_in_stock() ) { - echo '' . __( 'In stock', 'woocommerce' ) . ''; - } else { - echo '' . __( 'Out of stock', 'woocommerce' ) . ''; - } - break; - case 'stock_level' : - echo $product->get_stock_quantity(); - break; - case 'wc_actions' : - ?>

    - is_type( 'variation' ) ? $item->parent : $item->id; + // Get variation data. + if ( $product->is_type( 'variation' ) ) { + echo '

    ' . wp_kses_post( wc_get_formatted_variation( $product, true ) ) . '
    '; + } + break; - $actions['edit'] = array( - 'url' => admin_url( 'post.php?post=' . $action_id . '&action=edit' ), - 'name' => __( 'Edit', 'woocommerce' ), - 'action' => "edit" - ); + case 'parent' : + if ( $item->parent ) { + echo get_the_title( $item->parent ); + } else { + echo '-'; + } + break; - if ( $product->is_visible() ) - $actions['view'] = array( - 'url' => get_permalink( $action_id ), - 'name' => __( 'View', 'woocommerce' ), - 'action' => "view" - ); + case 'stock_status' : + if ( $product->is_in_stock() ) { + $stock_html = '' . __( 'In stock', 'woocommerce' ) . ''; + } else { + $stock_html = '' . __( 'Out of stock', 'woocommerce' ) . ''; + } + echo apply_filters( 'woocommerce_admin_stock_html', $stock_html, $product ); + break; - $actions = apply_filters( 'woocommerce_admin_stock_report_product_actions', $actions, $product ); + case 'stock_level' : + echo esc_html( $product->get_stock_quantity() ); + break; - foreach ( $actions as $action ) { - printf( '
    %s', $action['action'], esc_url( $action['url'] ), esc_attr( $action['name'] ), esc_attr( $action['name'] ) ); - } - ?> -

    + is_type( 'variation' ) ? $item->parent : $item->id; + + $actions['edit'] = array( + 'url' => admin_url( 'post.php?post=' . $action_id . '&action=edit' ), + 'name' => __( 'Edit', 'woocommerce' ), + 'action' => "edit", + ); + + if ( $product->is_visible() ) { + $actions['view'] = array( + 'url' => get_permalink( $action_id ), + 'name' => __( 'View', 'woocommerce' ), + 'action' => "view", + ); + } + + $actions = apply_filters( 'woocommerce_admin_stock_report_product_actions', $actions, $product ); + + foreach ( $actions as $action ) { + printf( + '%4$s', + esc_attr( $action['action'] ), + esc_url( $action['url'] ), + sprintf( esc_attr__( '%s product', 'woocommerce' ), $action['name'] ), + esc_html( $action['name'] ) + ); + } + ?> +

    __( 'Product', 'woocommerce' ), - 'parent' => __( 'Parent', 'woocommerce' ), - 'stock_level' => __( 'Units in stock', 'woocommerce' ), - 'stock_status' => __( 'Stock status', 'woocommerce' ), - 'wc_actions' => __( 'Actions', 'woocommerce' ), - ); + /** + * Get columns. + * + * @return array + */ + public function get_columns() { - return $columns; - } + $columns = array( + 'product' => __( 'Product', 'woocommerce' ), + 'parent' => __( 'Parent', 'woocommerce' ), + 'stock_level' => __( 'Units in stock', 'woocommerce' ), + 'stock_status' => __( 'Stock status', 'woocommerce' ), + 'wc_actions' => __( 'Actions', 'woocommerce' ), + ); - /** - * prepare_items function. - * - * @access public - */ - public function prepare_items() { - $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); - $current_page = absint( $this->get_pagenum() ); - $per_page = apply_filters( 'woocommerce_admin_stock_report_products_per_page', 20 ); + return $columns; + } - $this->get_items( $current_page, $per_page ); + /** + * Prepare customer list items. + */ + public function prepare_items() { - /** - * Pagination - */ - $this->set_pagination_args( array( - 'total_items' => $this->max_items, - 'per_page' => $per_page, - 'total_pages' => ceil( $this->max_items / $per_page ) - ) ); - } -} \ No newline at end of file + $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); + $current_page = absint( $this->get_pagenum() ); + $per_page = apply_filters( 'woocommerce_admin_stock_report_products_per_page', 20 ); + + $this->get_items( $current_page, $per_page ); + + /** + * Pagination. + */ + $this->set_pagination_args( array( + 'total_items' => $this->max_items, + 'per_page' => $per_page, + 'total_pages' => ceil( $this->max_items / $per_page ), + ) ); + } +} diff --git a/includes/admin/reports/class-wc-report-taxes-by-code.php b/includes/admin/reports/class-wc-report-taxes-by-code.php index 08ccfba6b34..7950e3bb002 100644 --- a/includes/admin/reports/class-wc-report-taxes-by-code.php +++ b/includes/admin/reports/class-wc-report-taxes-by-code.php @@ -1,33 +1,37 @@ @@ -37,76 +41,108 @@ class WC_Report_Taxes_By_Code extends WC_Admin_Report { } /** - * Output the report + * Output the report. */ public function output_report() { + $ranges = array( 'year' => __( 'Year', 'woocommerce' ), - 'last_month' => __( 'Last Month', 'woocommerce' ), - 'month' => __( 'This Month', 'woocommerce' ), + 'last_month' => __( 'Last month', 'woocommerce' ), + 'month' => __( 'This month', 'woocommerce' ), ); - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : 'last_month'; + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : 'last_month'; - if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) + if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = 'last_month'; + } + $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $hide_sidebar = true; - include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'); + include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php' ); } /** - * Get the main chart + * Get the main chart. + * * @return string */ public function get_main_chart() { global $wpdb; - $tax_rows = $this->get_order_report_data( array( - 'data' => array( - 'order_item_name' => array( - 'type' => 'order_item', - 'function' => '', - 'name' => 'tax_rate' - ), - 'tax_amount' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'tax', - 'function' => '', - 'name' => 'tax_amount' - ), - 'shipping_tax_amount' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'tax', - 'function' => '', - 'name' => 'shipping_tax_amount' - ), - 'rate_id' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'tax', - 'function' => '', - 'name' => 'rate_id' - ) + $query_data = array( + 'order_item_name' => array( + 'type' => 'order_item', + 'function' => '', + 'name' => 'tax_rate', ), - 'where' => array( - array( - 'key' => 'order_item_type', - 'value' => 'tax', - 'operator' => '=' - ), - array( - 'key' => 'order_item_name', - 'value' => '', - 'operator' => '!=' - ) + 'tax_amount' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'tax', + 'function' => '', + 'name' => 'tax_amount', ), - 'order_by' => 'post_date ASC', - 'query_type' => 'get_results', - 'filter_range' => true + 'shipping_tax_amount' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'tax', + 'function' => '', + 'name' => 'shipping_tax_amount', + ), + 'rate_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'tax', + 'function' => '', + 'name' => 'rate_id', + ), + 'ID' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'post_id', + ), + ); + + $query_where = array( + array( + 'key' => 'order_item_type', + 'value' => 'tax', + 'operator' => '=', + ), + array( + 'key' => 'order_item_name', + 'value' => '', + 'operator' => '!=', + ), + ); + + $tax_rows_orders = $this->get_order_report_data( array( + 'data' => $query_data, + 'where' => $query_where, + 'order_by' => 'posts.post_date ASC', + 'query_type' => 'get_results', + 'filter_range' => true, + 'order_types' => array_merge( wc_get_order_types( 'sales-reports' ), array( 'shop_order_refund' ) ), + 'order_status' => array( 'completed', 'processing', 'on-hold' ), + 'parent_order_status' => array( 'completed', 'processing', 'on-hold' ), // Partial refunds inside refunded orders should be ignored ) ); + + // Merge + $tax_rows = array(); + + foreach ( $tax_rows_orders as $tax_row ) { + $key = $tax_row->rate_id; + $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_orders' => 0 ); + + if ( 'shop_order_refund' !== get_post_type( $tax_row->post_id ) ) { + $tax_rows[ $key ]->total_orders += 1; + } + + $tax_rows[ $key ]->tax_rate = $tax_row->tax_rate; + $tax_rows[ $key ]->tax_amount += wc_round_tax_total( $tax_row->tax_amount ); + $tax_rows[ $key ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount ); + } ?>
    ' . wc_price( $product->order_item_total ) . '' . get_the_title( $product->product_id ) . '' . get_the_title( $product->product_id ) . ' ' . $this->sales_sparkline( $product->product_id, 7, 'sales' ) . '
    @@ -114,37 +150,20 @@ class WC_Report_Taxes_By_Code extends WC_Admin_Report { - - - + + + - + rate_id ] ) ) { - $grouped_tax_tows[ $tax_row->rate_id ] = (object) array( - 'tax_rate' => $tax_row->tax_rate, - 'total_orders' => 0, - 'tax_amount' => 0, - 'shipping_tax_amount' => 0 - ); - } - - $grouped_tax_tows[ $tax_row->rate_id ]->total_orders ++; - $grouped_tax_tows[ $tax_row->rate_id ]->tax_amount += wc_round_tax_total( $tax_row->tax_amount ); - $grouped_tax_tows[ $tax_row->rate_id ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount ); - } - - foreach ( $grouped_tax_tows as $rate_id => $tax_row ) { + foreach ( $tax_rows as $rate_id => $tax_row ) { $rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $rate_id ) ); ?> - - + + @@ -172,4 +191,4 @@ class WC_Report_Taxes_By_Code extends WC_Admin_Report {
    " href="#">[?] " href="#">[?] [?]
    tax_rate; ?>%tax_rate, $rate_id, $tax_row ); ?>% total_orders; ?> tax_amount ); ?> shipping_tax_amount ); ?>
    @@ -37,100 +41,134 @@ class WC_Report_Taxes_By_Date extends WC_Admin_Report { } /** - * Output the report + * Output the report. */ public function output_report() { + $ranges = array( 'year' => __( 'Year', 'woocommerce' ), - 'last_month' => __( 'Last Month', 'woocommerce' ), - 'month' => __( 'This Month', 'woocommerce' ), + 'last_month' => __( 'Last month', 'woocommerce' ), + 'month' => __( 'This month', 'woocommerce' ), ); - $current_range = ! empty( $_GET['range'] ) ? $_GET['range'] : 'last_month'; + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : 'last_month'; - if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) + if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = 'last_month'; + } + $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $hide_sidebar = true; - include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'); + include( WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php' ); } /** - * Get the main chart + * Get the main chart. + * * @return string */ public function get_main_chart() { - $tax_rows = $this->get_order_report_data( array( - 'data' => array( - '_order_tax' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'tax_amount' - ), - '_order_shipping_tax' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'shipping_tax_amount' - ), - '_order_total' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_sales' - ), - '_order_shipping' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_shipping' - ), - 'ID' => array( - 'type' => 'post_data', - 'function' => 'COUNT', - 'name' => 'total_orders', - 'distinct' => true, - ), - 'post_date' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'post_date' - ), + $query_data = array( + '_order_tax' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'tax_amount', ), + '_order_shipping_tax' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'shipping_tax_amount', + ), + '_order_total' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_sales', + ), + '_order_shipping' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_shipping', + ), + 'ID' => array( + 'type' => 'post_data', + 'function' => 'COUNT', + 'name' => 'total_orders', + 'distinct' => true, + ), + 'post_date' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'post_date', + ), + ); + + $tax_rows_orders = $this->get_order_report_data( array( + 'data' => $query_data, 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', - 'filter_range' => true + 'filter_range' => true, + 'order_types' => wc_get_order_types( 'sales-reports' ), + 'order_status' => array( 'completed', 'processing', 'on-hold' ), ) ); + + $tax_rows_partial_refunds = $this->get_order_report_data( array( + 'data' => $query_data, + 'group_by' => $this->group_by_query, + 'order_by' => 'post_date ASC', + 'query_type' => 'get_results', + 'filter_range' => true, + 'order_types' => array( 'shop_order_refund' ), + 'parent_order_status' => array( 'completed', 'processing', 'on-hold' ),// Partial refunds inside refunded orders should be ignored + ) ); + + // Merge + $tax_rows = array(); + + foreach ( $tax_rows_orders as $tax_row ) { + $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); + $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_sales' => 0, 'total_shipping' => 0, 'total_orders' => 0 ); + $tax_rows[ $key ]->tax_amount += $tax_row->tax_amount; + $tax_rows[ $key ]->shipping_tax_amount += $tax_row->shipping_tax_amount; + $tax_rows[ $key ]->total_sales += $tax_row->total_sales; + $tax_rows[ $key ]->total_shipping += $tax_row->total_shipping; + $tax_rows[ $key ]->total_orders += $tax_row->total_orders; + } + + foreach ( $tax_rows_partial_refunds as $tax_row ) { + $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); + $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_sales' => 0, 'total_shipping' => 0, 'total_orders' => 0 ); + $tax_rows[ $key ]->tax_amount += $tax_row->tax_amount; + $tax_rows[ $key ]->shipping_tax_amount += $tax_row->shipping_tax_amount; + $tax_rows[ $key ]->total_sales += $tax_row->total_sales; + $tax_rows[ $key ]->total_shipping += $tax_row->total_shipping; + } ?> - - - - + + + + - + $tax_row ) { $gross = $tax_row->total_sales - $tax_row->total_shipping; $total_tax = $tax_row->tax_amount + $tax_row->shipping_tax_amount; ?> - + @@ -142,6 +180,10 @@ class WC_Report_Taxes_By_Date extends WC_Admin_Report { ?> + @@ -161,4 +203,4 @@ class WC_Report_Taxes_By_Date extends WC_Admin_Report {
    " href="#">[?] " href="#">[?] [?] " href="#">[?]
    chart_groupby == 'month' ) - echo date_i18n( 'F', strtotime( $tax_row->post_date ) ); - else - echo date_i18n( get_option( 'date_format' ), strtotime( $tax_row->post_date ) ); - ?> + chart_groupby ) ? date_i18n( 'F', strtotime( $date . '01' ) ) : date_i18n( get_option( 'date_format' ), strtotime( $date ) ); ?> + total_orders; ?> total_shipping ); ?>
    id = 'account'; $this->label = __( 'Accounts', 'woocommerce' ); @@ -30,116 +33,67 @@ class WC_Settings_Accounts extends WC_Settings_Page { } /** - * Get settings array + * Get settings array. * * @return array */ public function get_settings() { + $settings = apply_filters( 'woocommerce_' . $this->id . '_settings', array( - return apply_filters( 'woocommerce_' . $this->id . '_settings', array( - - array( 'title' => __( 'Account Pages', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'These pages need to be set so that WooCommerce knows where to send users to access account related functionality.', 'woocommerce' ), 'id' => 'account_page_options' ), + array( 'title' => __( 'Account pages', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'These pages need to be set so that WooCommerce knows where to send users to access account related functionality.', 'woocommerce' ), 'id' => 'account_page_options' ), array( - 'title' => __( 'My Account Page', 'woocommerce' ), - 'desc' => __( 'Page contents:', 'woocommerce' ) . ' [' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']', - 'id' => 'woocommerce_myaccount_page_id', - 'type' => 'single_select_page', - 'default' => '', - 'class' => 'chosen_select_nostd', - 'css' => 'min-width:300px;', - 'desc_tip' => true, + 'title' => __( 'My account page', 'woocommerce' ), + 'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) ), + 'id' => 'woocommerce_myaccount_page_id', + 'type' => 'single_select_page', + 'default' => '', + 'class' => 'wc-enhanced-select-nostd', + 'css' => 'min-width:300px;', + 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'account_page_options' ), - array( 'title' => __( 'My Account Endpoints', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'Endpoints are appended to your page URLs to handle specific actions on the accounts pages. They should be unique.', 'woocommerce' ), 'id' => 'account_endpoint_options' ), + array( 'title' => '', 'type' => 'title', 'id' => 'account_registration_options' ), array( - 'title' => __( 'View Order', 'woocommerce' ), - 'desc' => __( 'Endpoint for the My Account → View Order page', 'woocommerce' ), - 'id' => 'woocommerce_myaccount_view_order_endpoint', - 'type' => 'text', - 'default' => 'view-order', - 'desc_tip' => true, - ), - - array( - 'title' => __( 'Edit Account', 'woocommerce' ), - 'desc' => __( 'Endpoint for the My Account → Edit Account page', 'woocommerce' ), - 'id' => 'woocommerce_myaccount_edit_account_endpoint', - 'type' => 'text', - 'default' => 'edit-account', - 'desc_tip' => true, - ), - - array( - 'title' => __( 'Edit Address', 'woocommerce' ), - 'desc' => __( 'Endpoint for the My Account → Edit Address page', 'woocommerce' ), - 'id' => 'woocommerce_myaccount_edit_address_endpoint', - 'type' => 'text', - 'default' => 'edit-address', - 'desc_tip' => true, - ), - - array( - 'title' => __( 'Lost Password', 'woocommerce' ), - 'desc' => __( 'Endpoint for the My Account → Lost Password page', 'woocommerce' ), - 'id' => 'woocommerce_myaccount_lost_password_endpoint', - 'type' => 'text', - 'default' => 'lost-password', - 'desc_tip' => true, - ), - - array( - 'title' => __( 'Logout', 'woocommerce' ), - 'desc' => __( 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', 'woocommerce' ), - 'id' => 'woocommerce_logout_endpoint', - 'type' => 'text', - 'default' => 'customer-logout', - 'desc_tip' => true, - ), - - array( 'type' => 'sectionend', 'id' => 'account_endpoint_options' ), - - array( 'title' => __( 'Registration Options', 'woocommerce' ), 'type' => 'title', 'id' => 'account_registration_options' ), - - array( - 'title' => __( 'Enable Registration', 'woocommerce' ), - 'desc' => __( 'Enable registration on the "Checkout" page', 'woocommerce' ), + 'title' => __( 'Customer registration', 'woocommerce' ), + 'desc' => __( 'Enable customer registration on the "Checkout" page.', 'woocommerce' ), 'id' => 'woocommerce_enable_signup_and_login_from_checkout', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', - 'autoload' => false + 'autoload' => false, ), array( - 'desc' => __( 'Enable registration on the "My Account" page', 'woocommerce' ), + 'desc' => __( 'Enable customer registration on the "My account" page.', 'woocommerce' ), 'id' => 'woocommerce_enable_myaccount_registration', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'end', - 'autoload' => false + 'autoload' => false, ), array( - 'desc' => __( 'Display returning customer login reminder on the "Checkout" page', 'woocommerce' ), + 'title' => __( 'Login', 'woocommerce' ), + 'desc' => __( 'Display returning customer login reminder on the "Checkout" page.', 'woocommerce' ), 'id' => 'woocommerce_enable_checkout_login_reminder', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', - 'autoload' => false + 'autoload' => false, ), array( - 'title' => __( 'Account Creation', 'woocommerce' ), - 'desc' => __( 'Automatically generate username from customer email', 'woocommerce' ), + 'title' => __( 'Account creation', 'woocommerce' ), + 'desc' => __( 'Automatically generate username from customer email.', 'woocommerce' ), 'id' => 'woocommerce_registration_generate_username', 'default' => 'yes', 'type' => 'checkbox', 'checkboxgroup' => 'start', - 'autoload' => false + 'autoload' => false, ), array( @@ -148,15 +102,93 @@ class WC_Settings_Accounts extends WC_Settings_Page { 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'end', - 'autoload' => false + 'autoload' => false, ), - array( 'type' => 'sectionend', 'id' => 'account_registration_options'), + array( 'type' => 'sectionend', 'id' => 'account_registration_options' ), - )); // End pages settings + array( 'title' => __( 'My account endpoints', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'Endpoints are appended to your page URLs to handle specific actions on the accounts pages. They should be unique and can be left blank to disable the endpoint.', 'woocommerce' ), 'id' => 'account_endpoint_options' ), + + array( + 'title' => __( 'Orders', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "My account → Orders" page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_orders_endpoint', + 'type' => 'text', + 'default' => 'orders', + 'desc_tip' => true, + ), + + array( + 'title' => __( 'View order', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "My account → View order" page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_view_order_endpoint', + 'type' => 'text', + 'default' => 'view-order', + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Downloads', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "My account → Downloads" page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_downloads_endpoint', + 'type' => 'text', + 'default' => 'downloads', + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Edit account', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "My account → Edit account" page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_edit_account_endpoint', + 'type' => 'text', + 'default' => 'edit-account', + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Addresses', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "My account → Addresses" page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_edit_address_endpoint', + 'type' => 'text', + 'default' => 'edit-address', + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Payment methods', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "My account → Payment methods" page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_payment_methods_endpoint', + 'type' => 'text', + 'default' => 'payment-methods', + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Lost password', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "My account → Lost password" page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_lost_password_endpoint', + 'type' => 'text', + 'default' => 'lost-password', + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Logout', 'woocommerce' ), + 'desc' => __( 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', 'woocommerce' ), + 'id' => 'woocommerce_logout_endpoint', + 'type' => 'text', + 'default' => 'customer-logout', + 'desc_tip' => true, + ), + + array( 'type' => 'sectionend', 'id' => 'account_endpoint_options' ), + + ) ); + + return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings ); } } endif; -return new WC_Settings_Accounts(); \ No newline at end of file +return new WC_Settings_Accounts(); diff --git a/includes/admin/settings/class-wc-settings-api.php b/includes/admin/settings/class-wc-settings-api.php new file mode 100644 index 00000000000..d5e304c39b1 --- /dev/null +++ b/includes/admin/settings/class-wc-settings-api.php @@ -0,0 +1,166 @@ +id = 'api'; + $this->label = __( 'API', 'woocommerce' ); + + add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 ); + add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) ); + add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) ); + add_action( 'woocommerce_settings_form_method_tab_' . $this->id, array( $this, 'form_method' ) ); + add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) ); + + $this->notices(); + } + + /** + * Get sections. + * + * @return array + */ + public function get_sections() { + $sections = array( + '' => __( 'Settings', 'woocommerce' ), + 'keys' => __( 'Keys/Apps', 'woocommerce' ), + 'webhooks' => __( 'Webhooks', 'woocommerce' ), + ); + + return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); + } + + /** + * Get settings array. + * + * @param string $current_section + * @return array + */ + public function get_settings( $current_section = '' ) { + $settings = array(); + + if ( '' === $current_section ) { + $settings = apply_filters( 'woocommerce_settings_rest_api', array( + array( + 'title' => __( 'General options', 'woocommerce' ), + 'type' => 'title', + 'desc' => '', + 'id' => 'general_options', + ), + + array( + 'title' => __( 'API', 'woocommerce' ), + 'desc' => __( 'Enable the REST API', 'woocommerce' ), + 'id' => 'woocommerce_api_enabled', + 'type' => 'checkbox', + 'default' => 'yes', + ), + + array( + 'type' => 'sectionend', + 'id' => 'general_options', + ), + ) ); + } + + return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $current_section ); + } + + /** + * Form method. + * + * @param string $method + * + * @return string + */ + public function form_method( $method ) { + global $current_section; + + if ( 'webhooks' == $current_section ) { + if ( isset( $_GET['edit-webhook'] ) ) { + $webhook_id = absint( $_GET['edit-webhook'] ); + $webhook = new WC_Webhook( $webhook_id ); + + if ( 'trash' != $webhook->post_data->post_status ) { + return 'post'; + } + } + + return 'get'; + } + + if ( 'keys' == $current_section ) { + if ( isset( $_GET['create-key'] ) || isset( $_GET['edit-key'] ) ) { + return 'post'; + } + + return 'get'; + } + + return 'post'; + } + + /** + * Notices. + */ + private function notices() { + if ( isset( $_GET['section'] ) && 'webhooks' == $_GET['section'] ) { + WC_Admin_Webhooks::notices(); + } + if ( isset( $_GET['section'] ) && 'keys' == $_GET['section'] ) { + WC_Admin_API_Keys::notices(); + } + } + + /** + * Output the settings. + */ + public function output() { + global $current_section; + + if ( 'webhooks' == $current_section ) { + WC_Admin_Webhooks::page_output(); + } elseif ( 'keys' === $current_section ) { + WC_Admin_API_Keys::page_output(); + } else { + $settings = $this->get_settings( $current_section ); + WC_Admin_Settings::output_fields( $settings ); + } + } + + /** + * Save settings. + */ + public function save() { + global $current_section; + + if ( apply_filters( 'woocommerce_rest_api_valid_to_save', ! in_array( $current_section, array( 'keys', 'webhooks' ) ) ) ) { + $settings = $this->get_settings(); + WC_Admin_Settings::save_fields( $settings ); + } + } +} + +endif; + +return new WC_Settings_Rest_API(); diff --git a/includes/admin/settings/class-wc-settings-checkout.php b/includes/admin/settings/class-wc-settings-checkout.php index a3f132e6b22..68af3138184 100644 --- a/includes/admin/settings/class-wc-settings-checkout.php +++ b/includes/admin/settings/class-wc-settings-checkout.php @@ -2,18 +2,20 @@ /** * WooCommerce Shipping Settings * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin - * @version 2.1.0 + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin + * @version 2.5.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; +} -if ( ! class_exists( 'WC_Settings_Payment_Gateways' ) ) : +if ( ! class_exists( 'WC_Settings_Payment_Gateways', false ) ) : /** - * WC_Settings_Payment_Gateways + * WC_Settings_Payment_Gateways. */ class WC_Settings_Payment_Gateways extends WC_Settings_Page { @@ -21,6 +23,7 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { * Constructor. */ public function __construct() { + $this->id = 'checkout'; $this->label = _x( 'Checkout', 'Settings tab label', 'woocommerce' ); @@ -32,175 +35,242 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { } /** - * Get sections + * Get sections. * * @return array */ public function get_sections() { $sections = array( - '' => __( 'Checkout Options', 'woocommerce' ) + '' => __( 'Checkout options', 'woocommerce' ), ); - // Load shipping methods so we can show any global options they may have - $payment_gateways = WC()->payment_gateways->payment_gateways(); + if ( ! defined( 'WC_INSTALLING' ) ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); - foreach ( $payment_gateways as $gateway ) { - - $title = empty( $gateway->method_title ) ? ucfirst( $gateway->id ) : $gateway->method_title; - - $sections[ strtolower( get_class( $gateway ) ) ] = esc_html( $title ); + foreach ( $payment_gateways as $gateway ) { + $title = empty( $gateway->method_title ) ? ucfirst( $gateway->id ) : $gateway->method_title; + $sections[ strtolower( $gateway->id ) ] = esc_html( $title ); + } } return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); } /** - * Get settings array + * Get settings array. + * + * @param string $current_section * * @return array */ - public function get_settings() { - return apply_filters( 'woocommerce_payment_gateways_settings', array( + public function get_settings( $current_section = '' ) { + $settings = array(); - array( 'title' => __( 'Checkout Process', 'woocommerce' ), 'type' => 'title', 'id' => 'checkout_process_options' ), + if ( '' === $current_section ) { + $settings = apply_filters( 'woocommerce_payment_gateways_settings', array( - array( - 'title' => __( 'Coupons', 'woocommerce' ), - 'desc' => __( 'Enable the use of coupons', 'woocommerce' ), - 'id' => 'woocommerce_enable_coupons', - 'default' => 'yes', - 'type' => 'checkbox', - 'desc_tip' => __( 'Coupons can be applied from the cart and checkout pages.', 'woocommerce' ), - 'autoload' => false - ), + array( + 'title' => __( 'Checkout process', 'woocommerce' ), + 'type' => 'title', + 'id' => 'checkout_process_options', + ), - array( - 'title' => _x( 'Checkout', 'Settings group label', 'woocommerce' ), - 'desc' => __( 'Enable guest checkout', 'woocommerce' ), - 'desc_tip' => __( 'Allows customers to checkout without creating an account.', 'woocommerce' ), - 'id' => 'woocommerce_enable_guest_checkout', - 'default' => 'yes', - 'type' => 'checkbox', - 'checkboxgroup' => 'start', - 'autoload' => false - ), + array( + 'title' => __( 'Coupons', 'woocommerce' ), + 'desc' => __( 'Enable the use of coupons', 'woocommerce' ), + 'id' => 'woocommerce_enable_coupons', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + 'desc_tip' => __( 'Coupons can be applied from the cart and checkout pages.', 'woocommerce' ), + ), - array( - 'desc' => __( 'Force secure checkout', 'woocommerce' ), - 'id' => 'woocommerce_force_ssl_checkout', - 'default' => 'no', - 'type' => 'checkbox', - 'checkboxgroup' => '', - 'show_if_checked' => 'option', - 'desc_tip' => __( 'Force SSL (HTTPS) on the checkout pages (an SSL Certificate is required).', 'woocommerce' ), - ), + array( + 'desc' => __( 'Calculate coupon discounts sequentially', 'woocommerce' ), + 'id' => 'woocommerce_calc_discounts_sequentially', + 'default' => 'no', + 'type' => 'checkbox', + 'desc_tip' => __( 'When applying multiple coupons, apply the first coupon to the full price and the second coupon to the discounted price and so on.', 'woocommerce' ), + 'checkboxgroup' => 'end', + 'autoload' => false, + ), - array( - 'desc' => __( 'Force HTTP when leaving the checkout', 'woocommerce' ), - 'id' => 'woocommerce_unforce_ssl_checkout', - 'default' => 'no', - 'type' => 'checkbox', - 'checkboxgroup' => 'end', - 'show_if_checked' => 'yes', - ), + array( + 'title' => __( 'Checkout process', 'woocommerce' ), + 'desc' => __( 'Enable guest checkout', 'woocommerce' ), + 'desc_tip' => __( 'Allows customers to checkout without creating an account.', 'woocommerce' ), + 'id' => 'woocommerce_enable_guest_checkout', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + 'autoload' => false, + ), - array( 'type' => 'sectionend', 'id' => 'checkout_process_options'), + array( + 'desc' => __( 'Force secure checkout', 'woocommerce' ), + 'id' => 'woocommerce_force_ssl_checkout', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => '', + 'show_if_checked' => 'option', + 'desc_tip' => sprintf( __( 'Force SSL (HTTPS) on the checkout pages (
    an SSL Certificate is required).', 'woocommerce' ), 'https://docs.woocommerce.com/document/ssl-and-https/#section-3' ), + ), - array( 'title' => __( 'Checkout Pages', 'woocommerce' ), 'desc' => __( 'These pages need to be set so that WooCommerce knows where to send users to checkout.', 'woocommerce' ), 'type' => 'title', 'id' => 'checkout_page_options' ), + 'unforce_ssl_checkout' => array( + 'desc' => __( 'Force HTTP when leaving the checkout', 'woocommerce' ), + 'id' => 'woocommerce_unforce_ssl_checkout', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'end', + 'show_if_checked' => 'yes', + ), - array( - 'title' => __( 'Cart Page', 'woocommerce' ), - 'desc' => __( 'Page contents:', 'woocommerce' ) . ' [' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', - 'id' => 'woocommerce_cart_page_id', - 'type' => 'single_select_page', - 'default' => '', - 'class' => 'chosen_select_nostd', - 'css' => 'min-width:300px;', - 'desc_tip' => true, - ), + array( + 'type' => 'sectionend', + 'id' => 'checkout_process_options', + ), - array( - 'title' => __( 'Checkout Page', 'woocommerce' ), - 'desc' => __( 'Page contents:', 'woocommerce' ) . ' [' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', - 'id' => 'woocommerce_checkout_page_id', - 'type' => 'single_select_page', - 'default' => '', - 'class' => 'chosen_select_nostd', - 'css' => 'min-width:300px;', - 'desc_tip' => true, - ), + array( + 'title' => __( 'Checkout pages', 'woocommerce' ), + 'desc' => __( 'These pages need to be set so that WooCommerce knows where to send users to checkout.', 'woocommerce' ), + 'type' => 'title', + 'id' => 'checkout_page_options', + ), - array( - 'title' => __( 'Terms and Conditions', 'woocommerce' ), - 'desc' => __( 'If you define a "Terms" page the customer will be asked if they accept them when checking out.', 'woocommerce' ), - 'id' => 'woocommerce_terms_page_id', - 'default' => '', - 'class' => 'chosen_select_nostd', - 'css' => 'min-width:300px;', - 'type' => 'single_select_page', - 'desc_tip' => true, - 'autoload' => false - ), + array( + 'title' => __( 'Cart page', 'woocommerce' ), + 'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) ), + 'id' => 'woocommerce_cart_page_id', + 'type' => 'single_select_page', + 'default' => '', + 'class' => 'wc-enhanced-select-nostd', + 'css' => 'min-width:300px;', + 'desc_tip' => true, + ), - array( 'type' => 'sectionend', 'id' => 'checkout_page_options' ), + array( + 'title' => __( 'Checkout page', 'woocommerce' ), + 'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) ), + 'id' => 'woocommerce_checkout_page_id', + 'type' => 'single_select_page', + 'default' => '', + 'class' => 'wc-enhanced-select-nostd', + 'css' => 'min-width:300px;', + 'desc_tip' => true, + ), - array( 'title' => __( 'Checkout Endpoints', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'Endpoints are appended to your page URLs to handle specific actions during the checkout process. They should be unique.', 'woocommerce' ), 'id' => 'account_endpoint_options' ), + array( + 'title' => __( 'Terms and conditions', 'woocommerce' ), + 'desc' => __( 'If you define a "Terms" page the customer will be asked if they accept them when checking out.', 'woocommerce' ), + 'id' => 'woocommerce_terms_page_id', + 'default' => '', + 'class' => 'wc-enhanced-select-nostd', + 'css' => 'min-width:300px;', + 'type' => 'single_select_page', + 'desc_tip' => true, + 'autoload' => false, + ), - array( - 'title' => __( 'Pay', 'woocommerce' ), - 'desc' => __( 'Endpoint for the Checkout → Pay page', 'woocommerce' ), - 'id' => 'woocommerce_checkout_pay_endpoint', - 'type' => 'text', - 'default' => 'order-pay', - 'desc_tip' => true, - ), + array( + 'type' => 'sectionend', + 'id' => 'checkout_page_options', + ), - array( - 'title' => __( 'Order Received', 'woocommerce' ), - 'desc' => __( 'Endpoint for the Checkout → Order Received page', 'woocommerce' ), - 'id' => 'woocommerce_checkout_order_received_endpoint', - 'type' => 'text', - 'default' => 'order-received', - 'desc_tip' => true, - ), + array( 'title' => __( 'Checkout endpoints', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'Endpoints are appended to your page URLs to handle specific actions during the checkout process. They should be unique.', 'woocommerce' ), 'id' => 'account_endpoint_options' ), - array( - 'title' => __( 'Add Payment Method', 'woocommerce' ), - 'desc' => __( 'Endpoint for the Checkout → Add Payment Method page', 'woocommerce' ), - 'id' => 'woocommerce_myaccount_add_payment_method_endpoint', - 'type' => 'text', - 'default' => 'add-payment-method', - 'desc_tip' => true, - ), + array( + 'title' => __( 'Pay', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "Checkout → Pay" page.', 'woocommerce' ), + 'id' => 'woocommerce_checkout_pay_endpoint', + 'type' => 'text', + 'default' => 'order-pay', + 'desc_tip' => true, + ), - array( 'type' => 'sectionend', 'id' => 'checkout_endpoint_options' ), + array( + 'title' => __( 'Order received', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "Checkout → Order received" page.', 'woocommerce' ), + 'id' => 'woocommerce_checkout_order_received_endpoint', + 'type' => 'text', + 'default' => 'order-received', + 'desc_tip' => true, + ), - array( 'title' => __( 'Payment Gateways', 'woocommerce' ), 'desc' => __( 'Installed gateways are listed below. Drag and drop gateways to control their display order on the frontend.', 'woocommerce' ), 'type' => 'title', 'id' => 'payment_gateways_options' ), + array( + 'title' => __( 'Add payment method', 'woocommerce' ), + 'desc' => __( 'Endpoint for the "Checkout → Add payment method" page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_add_payment_method_endpoint', + 'type' => 'text', + 'default' => 'add-payment-method', + 'desc_tip' => true, + ), - array( 'type' => 'payment_gateways' ), + array( + 'title' => __( 'Delete payment method', 'woocommerce' ), + 'desc' => __( 'Endpoint for the delete payment method page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_delete_payment_method_endpoint', + 'type' => 'text', + 'default' => 'delete-payment-method', + 'desc_tip' => true, + ), - array( 'type' => 'sectionend', 'id' => 'payment_gateways_options' ), + array( + 'title' => __( 'Set default payment method', 'woocommerce' ), + 'desc' => __( 'Endpoint for the setting a default payment method page.', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_set_default_payment_method_endpoint', + 'type' => 'text', + 'default' => 'set-default-payment-method', + 'desc_tip' => true, + ), - )); // End payment_gateway settings + array( + 'type' => 'sectionend', + 'id' => 'checkout_endpoint_options', + ), + + array( + 'title' => __( 'Payment gateways', 'woocommerce' ), + 'desc' => __( 'Installed gateways are listed below. Drag and drop gateways to control their display order on the frontend.', 'woocommerce' ), + 'type' => 'title', + 'id' => 'payment_gateways_options', + ), + + array( + 'type' => 'payment_gateways', + ), + + array( + 'type' => 'sectionend', + 'id' => 'payment_gateways_options', + ), + + ) ); + + if ( wc_site_is_https() ) { + unset( $settings['unforce_ssl_checkout'] ); + } + } + + return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $current_section ); } /** - * Output the settings + * Output the settings. */ public function output() { global $current_section; - // Load shipping methods so we can show any global options they may have + // Load shipping methods so we can show any global options they may have. $payment_gateways = WC()->payment_gateways->payment_gateways(); if ( $current_section ) { - foreach ( $payment_gateways as $gateway ) { - if ( strtolower( get_class( $gateway ) ) == strtolower( $current_section ) ) { + foreach ( $payment_gateways as $gateway ) { + if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ) ) ) { $gateway->admin_options(); break; } } - } else { + } else { $settings = $this->get_settings(); WC_Admin_Settings::output_fields( $settings ); @@ -209,25 +279,21 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { /** * Output payment gateway settings. - * - * @access public - * @return void */ public function payment_gateways_setting() { ?> - - + + __( 'Default', 'woocommerce' ), + 'sort' => '', 'name' => __( 'Gateway', 'woocommerce' ), 'id' => __( 'Gateway ID', 'woocommerce' ), - 'status' => __( 'Status', 'woocommerce' ), - 'settings' => '' + 'status' => __( 'Enabled', 'woocommerce' ), ) ); foreach ( $columns as $key => $column ) { @@ -237,55 +303,47 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { - payment_gateways->payment_gateways() as $gateway ) { - foreach ( WC()->payment_gateways->payment_gateways() as $gateway ) { + echo ''; - echo ''; + foreach ( $columns as $key => $column ) { - foreach ( $columns as $key => $column ) { switch ( $key ) { - case 'default' : - echo ''; - break; + + case 'sort' : + echo ''; + break; + case 'name' : + $method_title = $gateway->get_title() ? $gateway->get_title() : __( '(no title)', 'woocommerce' ); echo ''; - break; + ' . esc_html( $method_title ) . ' + '; + break; + case 'id' : - echo ''; - break; + echo ''; + break; + case 'status' : echo ''; + break; - if ( $gateway->enabled == 'yes' ) - echo '' . __ ( 'Enabled', 'woocommerce' ) . ''; - else - echo '-'; - - echo ''; - break; - case 'settings' : - echo ''; - break; default : do_action( 'woocommerce_payment_gateways_setting_column_' . $key, $gateway ); - break; + break; } } echo ''; - } - ?> + } + ?>
    - id ), false ) . ' /> - - + + - ' . $gateway->get_title() . ' - - ' . esc_html( $gateway->id ) . ' - ' . esc_html( $gateway->id ) . ''; + echo ( 'yes' === $gateway->enabled ) ? '' . esc_html__( 'Yes', 'woocommerce' ) . '' : '-'; + echo ' - ' . __( 'Settings', 'woocommerce' ) . ' -
    @@ -294,29 +352,28 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { } /** - * Save settings + * Save settings. */ public function save() { global $current_section; + $wc_payment_gateways = WC_Payment_Gateways::instance(); + if ( ! $current_section ) { + WC_Admin_Settings::save_fields( $this->get_settings() ); + $wc_payment_gateways->process_admin_options(); - $settings = $this->get_settings(); - - WC_Admin_Settings::save_fields( $settings ); - WC()->payment_gateways->process_admin_options(); - - } elseif ( class_exists( $current_section ) ) { - - $current_section_class = new $current_section(); - - do_action( 'woocommerce_update_options_payment_gateways_' . $current_section_class->id ); - - WC()->payment_gateways()->init(); + } else { + foreach ( $wc_payment_gateways->payment_gateways() as $gateway ) { + if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ) ) ) { + do_action( 'woocommerce_update_options_payment_gateways_' . $gateway->id ); + $wc_payment_gateways->init(); + } + } } } } endif; -return new WC_Settings_Payment_Gateways(); \ No newline at end of file +return new WC_Settings_Payment_Gateways(); diff --git a/includes/admin/settings/class-wc-settings-emails.php b/includes/admin/settings/class-wc-settings-emails.php index 015062411b9..79ce1838dbb 100644 --- a/includes/admin/settings/class-wc-settings-emails.php +++ b/includes/admin/settings/class-wc-settings-emails.php @@ -2,18 +2,20 @@ /** * WooCommerce Email Settings * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} -if ( ! class_exists( 'WC_Settings_Emails' ) ) : +if ( ! class_exists( 'WC_Settings_Emails', false ) ) : /** - * WC_Settings_Emails + * WC_Settings_Emails. */ class WC_Settings_Emails extends WC_Settings_Page { @@ -28,186 +30,281 @@ class WC_Settings_Emails extends WC_Settings_Page { add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) ); add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) ); add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) ); + add_action( 'woocommerce_admin_field_email_notification', array( $this, 'email_notification_setting' ) ); } /** - * Get sections + * Get sections. * * @return array */ public function get_sections() { $sections = array( - '' => __( 'Email Options', 'woocommerce' ) + '' => __( 'Email options', 'woocommerce' ), ); - - // Define emails that can be customised here - $mailer = WC()->mailer(); - $email_templates = $mailer->get_emails(); - - foreach ( $email_templates as $email ) { - $title = empty( $email->title ) ? ucfirst( $email->id ) : ucfirst( $email->title ); - - $sections[ strtolower( get_class( $email ) ) ] = esc_html( $title ); - } - return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); } /** - * Get settings array + * Get settings array. * * @return array */ public function get_settings() { - return apply_filters('woocommerce_email_settings', array( + $settings = apply_filters( 'woocommerce_email_settings', array( + + array( 'title' => __( 'Email notifications', 'woocommerce' ), 'desc' => __( 'Email notifications sent from WooCommerce are listed below. Click on an email to configure it.', 'woocommerce' ), 'type' => 'title', 'id' => 'email_notification_settings' ), + + array( 'type' => 'email_notification' ), + + array( 'type' => 'sectionend', 'id' => 'email_notification_settings' ), array( 'type' => 'sectionend', 'id' => 'email_recipient_options' ), - array( 'title' => __( 'Email Sender Options', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'The following options affect the sender (email address and name) used in WooCommerce emails.', 'woocommerce' ), 'id' => 'email_options' ), + array( 'title' => __( 'Email sender options', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'email_options' ), array( - 'title' => __( '"From" Name', 'woocommerce' ), - 'desc' => '', - 'id' => 'woocommerce_email_from_name', - 'type' => 'text', - 'css' => 'min-width:300px;', - 'default' => esc_attr(get_bloginfo('title')), - 'autoload' => false + 'title' => __( '"From" name', 'woocommerce' ), + 'desc' => __( 'How the sender name appears in outgoing WooCommerce emails.', 'woocommerce' ), + 'id' => 'woocommerce_email_from_name', + 'type' => 'text', + 'css' => 'min-width:300px;', + 'default' => esc_attr( get_bloginfo( 'name', 'display' ) ), + 'autoload' => false, + 'desc_tip' => true, ), array( - 'title' => __( '"From" Email Address', 'woocommerce' ), - 'desc' => '', - 'id' => 'woocommerce_email_from_address', - 'type' => 'email', + 'title' => __( '"From" address', 'woocommerce' ), + 'desc' => __( 'How the sender email appears in outgoing WooCommerce emails.', 'woocommerce' ), + 'id' => 'woocommerce_email_from_address', + 'type' => 'email', 'custom_attributes' => array( - 'multiple' => 'multiple' + 'multiple' => 'multiple', ), - 'css' => 'min-width:300px;', - 'default' => get_option('admin_email'), - 'autoload' => false + 'css' => 'min-width:300px;', + 'default' => get_option( 'admin_email' ), + 'autoload' => false, + 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'email_options' ), - array( 'title' => __( 'Email Template', 'woocommerce' ), 'type' => 'title', 'desc' => sprintf(__( 'This section lets you customise the WooCommerce emails. Click here to preview your email template. For more advanced control copy woocommerce/templates/emails/ to yourtheme/woocommerce/emails/.', 'woocommerce' ), wp_nonce_url(admin_url('?preview_woocommerce_mail=true'), 'preview-mail')), 'id' => 'email_template_options' ), + array( 'title' => __( 'Email template', 'woocommerce' ), 'type' => 'title', 'desc' => sprintf( __( 'This section lets you customize the WooCommerce emails. Click here to preview your email template.', 'woocommerce' ), wp_nonce_url( admin_url( '?preview_woocommerce_mail=true' ), 'preview-mail' ) ), 'id' => 'email_template_options' ), array( - 'title' => __( 'Header Image', 'woocommerce' ), - 'desc' => sprintf(__( 'Enter a URL to an image you want to show in the email\'s header. Upload your image using the media uploader.', 'woocommerce' ), admin_url('media-new.php')), - 'id' => 'woocommerce_email_header_image', - 'type' => 'text', - 'css' => 'min-width:300px;', - 'default' => '', - 'autoload' => false + 'title' => __( 'Header image', 'woocommerce' ), + 'desc' => __( 'URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).', 'woocommerce' ), + 'id' => 'woocommerce_email_header_image', + 'type' => 'text', + 'css' => 'min-width:300px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'default' => '', + 'autoload' => false, + 'desc_tip' => true, ), array( - 'title' => __( 'Email Footer Text', 'woocommerce' ), - 'desc' => __( 'The text to appear in the footer of WooCommerce emails.', 'woocommerce' ), - 'id' => 'woocommerce_email_footer_text', - 'css' => 'width:100%; height: 75px;', - 'type' => 'textarea', - 'default' => get_bloginfo('title') . ' - ' . __( 'Powered by WooCommerce', 'woocommerce' ), - 'autoload' => false + 'title' => __( 'Footer text', 'woocommerce' ), + 'desc' => __( 'The text to appear in the footer of WooCommerce emails.', 'woocommerce' ), + 'id' => 'woocommerce_email_footer_text', + 'css' => 'width:300px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + /* translators: %s: site name */ + 'default' => sprintf( __( '%s - Powered by WooCommerce', 'woocommerce' ), get_bloginfo( 'name', 'display' ) ), + 'autoload' => false, + 'desc_tip' => true, ), array( - 'title' => __( 'Base Colour', 'woocommerce' ), - 'desc' => __( 'The base colour for WooCommerce email templates. Default #557da1.', 'woocommerce' ), - 'id' => 'woocommerce_email_base_color', - 'type' => 'color', - 'css' => 'width:6em;', - 'default' => '#557da1', - 'autoload' => false + 'title' => __( 'Base color', 'woocommerce' ), + /* translators: %s: default color */ + 'desc' => sprintf( __( 'The base color for WooCommerce email templates. Default %s.', 'woocommerce' ), '#96588a' ), + 'id' => 'woocommerce_email_base_color', + 'type' => 'color', + 'css' => 'width:6em;', + 'default' => '#96588a', + 'autoload' => false, + 'desc_tip' => true, ), array( - 'title' => __( 'Background Colour', 'woocommerce' ), - 'desc' => __( 'The background colour for WooCommerce email templates. Default #f5f5f5.', 'woocommerce' ), - 'id' => 'woocommerce_email_background_color', - 'type' => 'color', - 'css' => 'width:6em;', - 'default' => '#f5f5f5', - 'autoload' => false + 'title' => __( 'Background color', 'woocommerce' ), + /* translators: %s: default color */ + 'desc' => sprintf( __( 'The background color for WooCommerce email templates. Default %s.', 'woocommerce' ), '#f7f7f7' ), + 'id' => 'woocommerce_email_background_color', + 'type' => 'color', + 'css' => 'width:6em;', + 'default' => '#f7f7f7', + 'autoload' => false, + 'desc_tip' => true, ), array( - 'title' => __( 'Email Body Background Colour', 'woocommerce' ), - 'desc' => __( 'The main body background colour. Default #fdfdfd.', 'woocommerce' ), - 'id' => 'woocommerce_email_body_background_color', - 'type' => 'color', - 'css' => 'width:6em;', - 'default' => '#fdfdfd', - 'autoload' => false + 'title' => __( 'Body background color', 'woocommerce' ), + /* translators: %s: default color */ + 'desc' => sprintf( __( 'The main body background color. Default %s.', 'woocommerce' ), '#ffffff' ), + 'id' => 'woocommerce_email_body_background_color', + 'type' => 'color', + 'css' => 'width:6em;', + 'default' => '#ffffff', + 'autoload' => false, + 'desc_tip' => true, ), array( - 'title' => __( 'Email Body Text Colour', 'woocommerce' ), - 'desc' => __( 'The main body text colour. Default #505050.', 'woocommerce' ), - 'id' => 'woocommerce_email_text_color', - 'type' => 'color', - 'css' => 'width:6em;', - 'default' => '#505050', - 'autoload' => false + 'title' => __( 'Body text color', 'woocommerce' ), + /* translators: %s: default color */ + 'desc' => sprintf( __( 'The main body text color. Default %s.', 'woocommerce' ), '#3c3c3c' ), + 'id' => 'woocommerce_email_text_color', + 'type' => 'color', + 'css' => 'width:6em;', + 'default' => '#3c3c3c', + 'autoload' => false, + 'desc_tip' => true, ), array( 'type' => 'sectionend', 'id' => 'email_template_options' ), - )); // End email settings + ) ); + + return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings ); } /** - * Output the settings + * Output the settings. */ public function output() { global $current_section; // Define emails that can be customised here - $mailer = WC()->mailer(); - $email_templates = $mailer->get_emails(); + $mailer = WC()->mailer(); + $email_templates = $mailer->get_emails(); if ( $current_section ) { - foreach ( $email_templates as $email ) { - if ( strtolower( get_class( $email ) ) == $current_section ) { + foreach ( $email_templates as $email_key => $email ) { + if ( strtolower( $email_key ) == $current_section ) { $email->admin_options(); break; } } - } else { + } else { $settings = $this->get_settings(); - WC_Admin_Settings::output_fields( $settings ); } } /** - * Save settings + * Save settings. */ public function save() { global $current_section; if ( ! $current_section ) { - - $settings = $this->get_settings(); - WC_Admin_Settings::save_fields( $settings ); + WC_Admin_Settings::save_fields( $this->get_settings() ); } else { + $wc_emails = WC_Emails::instance(); - // Load mailer - $mailer = WC()->mailer(); - - if ( class_exists( $current_section ) ) { - $current_section_class = new $current_section(); - do_action( 'woocommerce_update_options_' . $this->id . '_' . $current_section_class->id ); - WC()->mailer()->init(); + if ( in_array( $current_section, array_map( 'sanitize_title', array_keys( $wc_emails->get_emails() ) ) ) ) { + foreach ( $wc_emails->get_emails() as $email_id => $email ) { + if ( sanitize_title( $email_id ) === $current_section ) { + do_action( 'woocommerce_update_options_' . $this->id . '_' . $email->id ); + } + } } else { do_action( 'woocommerce_update_options_' . $this->id . '_' . $current_section ); } } } + + /** + * Output email notification settings. + */ + public function email_notification_setting() { + // Define emails that can be customised here + $mailer = WC()->mailer(); + $email_templates = $mailer->get_emails(); + ?> + + + + + + '', + 'name' => __( 'Email', 'woocommerce' ), + 'email_type' => __( 'Content type', 'woocommerce' ), + 'recipient' => __( 'Recipient(s)', 'woocommerce' ), + 'actions' => '', + ) ); + foreach ( $columns as $key => $column ) { + echo ''; + } + ?> + + + + $email ) { + echo ''; + + foreach ( $columns as $key => $column ) { + + switch ( $key ) { + case 'name' : + echo ''; + break; + case 'recipient' : + echo ''; + break; + case 'status' : + echo ''; + break; + case 'email_type' : + echo ''; + break; + case 'actions' : + echo ''; + break; + default : + do_action( 'woocommerce_email_setting_column_' . $key, $email ); + break; + } + } + + echo ''; + } + ?> + +
    + + + id = 'general'; $this->label = __( 'General', 'woocommerce' ); add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 ); add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) ); add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) ); - - if ( ( $styles = WC_Frontend_Scripts::get_styles() ) && array_key_exists( 'woocommerce-general', $styles ) ) { - add_action( 'woocommerce_admin_field_frontend_styles', array( $this, 'frontend_styles_setting' ) ); - } } /** - * Get settings array + * Get settings array. * * @return array */ public function get_settings() { + $currency_code_options = get_woocommerce_currencies(); foreach ( $currency_code_options as $code => $name ) { $currency_code_options[ $code ] = $name . ' (' . get_woocommerce_currency_symbol( $code ) . ')'; } - return apply_filters( 'woocommerce_general_settings', array( - - array( 'title' => __( 'General Options', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'general_options' ), + $settings = apply_filters( 'woocommerce_general_settings', array( array( - 'title' => __( 'Base Location', 'woocommerce' ), - 'desc' => __( 'This is the base location for your business. Tax rates will be based on this country.', 'woocommerce' ), - 'id' => 'woocommerce_default_country', - 'css' => 'min-width:350px;', - 'default' => 'GB', - 'type' => 'single_select_country', - 'desc_tip' => true, + 'title' => __( 'Store Address', 'woocommerce' ), + 'type' => 'title', + 'desc' => __( 'This is where your business is located. Tax rates and shipping rates will use this address.', 'woocommerce' ), + 'id' => 'store_address', ), array( - 'title' => __( 'Selling Location(s)', 'woocommerce' ), - 'desc' => __( 'This option lets you limit which countries you are willing to sell to.', 'woocommerce' ), - 'id' => 'woocommerce_allowed_countries', - 'default' => 'all', - 'type' => 'select', - 'class' => 'chosen_select', - 'css' => 'min-width: 350px;', - 'desc_tip' => true, - 'options' => array( - 'all' => __( 'Sell to all countries', 'woocommerce' ), - 'specific' => __( 'Sell to specific countries only', 'woocommerce' ) - ) + 'title' => __( 'Address line 1', 'woocommerce' ), + 'desc' => __( 'The street address for your business location.', 'woocommerce' ), + 'id' => 'woocommerce_store_address', + 'default' => '', + 'type' => 'text', + 'desc_tip' => true, ), array( - 'title' => __( 'Specific Countries', 'woocommerce' ), - 'desc' => '', - 'id' => 'woocommerce_specific_allowed_countries', - 'css' => 'min-width: 350px;', - 'default' => '', - 'type' => 'multi_select_countries' + 'title' => __( 'Address line 2', 'woocommerce' ), + 'desc' => __( 'An additional, optional address line for your business location.', 'woocommerce' ), + 'id' => 'woocommerce_store_address_2', + 'default' => '', + 'type' => 'text', + 'desc_tip' => true, ), array( - 'title' => __( 'Store Notice', 'woocommerce' ), - 'desc' => __( 'Enable site-wide store notice text', 'woocommerce' ), - 'id' => 'woocommerce_demo_store', - 'default' => 'no', - 'type' => 'checkbox' + 'title' => __( 'City', 'woocommerce' ), + 'desc' => __( 'The city in which your business is located.', 'woocommerce' ), + 'id' => 'woocommerce_store_city', + 'default' => '', + 'type' => 'text', + 'desc_tip' => true, ), array( - 'title' => __( 'Store Notice Text', 'woocommerce' ), - 'desc' => '', - 'id' => 'woocommerce_demo_store_notice', - 'default' => __( 'This is a demo store for testing purposes — no orders shall be fulfilled.', 'woocommerce' ), - 'type' => 'text', - 'css' => 'min-width:300px;', - 'autoload' => false + 'title' => __( 'Country / State', 'woocommerce' ), + 'desc' => __( 'The country and state or province, if any, in which your business is located.', 'woocommerce' ), + 'id' => 'woocommerce_default_country', + 'default' => 'GB', + 'type' => 'single_select_country', + 'desc_tip' => true, ), array( - 'title' => __( 'API', 'woocommerce' ), - 'desc' => __( 'Enable the REST API', 'woocommerce' ), - 'id' => 'woocommerce_api_enabled', + 'title' => __( 'Postcode / ZIP', 'woocommerce' ), + 'desc' => __( 'The postal code, if any, in which your business is located.', 'woocommerce' ), + 'id' => 'woocommerce_store_postcode', + 'css' => 'min-width:50px;', + 'default' => '', + 'type' => 'text', + 'desc_tip' => true, + ), + + array( 'type' => 'sectionend', 'id' => 'store_address' ), + + array( 'title' => __( 'General options', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'general_options' ), + + array( + 'title' => __( 'Selling location(s)', 'woocommerce' ), + 'desc' => __( 'This option lets you limit which countries you are willing to sell to.', 'woocommerce' ), + 'id' => 'woocommerce_allowed_countries', + 'default' => 'all', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'css' => 'min-width: 350px;', + 'desc_tip' => true, + 'options' => array( + 'all' => __( 'Sell to all countries', 'woocommerce' ), + 'all_except' => __( 'Sell to all countries, except for…', 'woocommerce' ), + 'specific' => __( 'Sell to specific countries', 'woocommerce' ), + ), + ), + + array( + 'title' => __( 'Sell to all countries, except for…', 'woocommerce' ), + 'desc' => '', + 'id' => 'woocommerce_all_except_countries', + 'css' => 'min-width: 350px;', + 'default' => '', + 'type' => 'multi_select_countries', + ), + + array( + 'title' => __( 'Sell to specific countries', 'woocommerce' ), + 'desc' => '', + 'id' => 'woocommerce_specific_allowed_countries', + 'css' => 'min-width: 350px;', + 'default' => '', + 'type' => 'multi_select_countries', + ), + + array( + 'title' => __( 'Shipping location(s)', 'woocommerce' ), + 'desc' => __( 'Choose which countries you want to ship to, or choose to ship to all locations you sell to.', 'woocommerce' ), + 'id' => 'woocommerce_ship_to_countries', + 'default' => '', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'desc_tip' => true, + 'options' => array( + '' => __( 'Ship to all countries you sell to', 'woocommerce' ), + 'all' => __( 'Ship to all countries', 'woocommerce' ), + 'specific' => __( 'Ship to specific countries only', 'woocommerce' ), + 'disabled' => __( 'Disable shipping & shipping calculations', 'woocommerce' ), + ), + ), + + array( + 'title' => __( 'Ship to specific countries', 'woocommerce' ), + 'desc' => '', + 'id' => 'woocommerce_specific_ship_to_countries', + 'css' => '', + 'default' => '', + 'type' => 'multi_select_countries', + ), + + array( + 'title' => __( 'Default customer location', 'woocommerce' ), + 'id' => 'woocommerce_default_customer_address', + 'desc_tip' => __( 'This option determines a customers default location. The MaxMind GeoLite Database will be periodically downloaded to your wp-content directory if using geolocation.', 'woocommerce' ), + 'default' => 'geolocation', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'options' => array( + '' => __( 'No location by default', 'woocommerce' ), + 'base' => __( 'Shop base address', 'woocommerce' ), + 'geolocation' => __( 'Geolocate', 'woocommerce' ), + 'geolocation_ajax' => __( 'Geolocate (with page caching support)', 'woocommerce' ), + ), + ), + + array( + 'title' => __( 'Enable taxes', 'woocommerce' ), + 'desc' => __( 'Enable taxes and tax calculations', 'woocommerce' ), + 'id' => 'woocommerce_calc_taxes', + 'default' => 'no', 'type' => 'checkbox', - 'default' => 'yes', - ), - - array( 'type' => 'sectionend', 'id' => 'general_options'), - - array( 'title' => __( 'Currency Options', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'The following options affect how prices are displayed on the frontend.', 'woocommerce' ), 'id' => 'pricing_options' ), - - array( - 'title' => __( 'Currency', 'woocommerce' ), - 'desc' => __( 'This controls what currency prices are listed at in the catalog and which currency gateways will take payments in.', 'woocommerce' ), - 'id' => 'woocommerce_currency', - 'css' => 'min-width:350px;', - 'default' => 'GBP', - 'type' => 'select', - 'class' => 'chosen_select', - 'desc_tip' => true, - 'options' => $currency_code_options ), array( - 'title' => __( 'Currency Position', 'woocommerce' ), - 'desc' => __( 'This controls the position of the currency symbol.', 'woocommerce' ), - 'id' => 'woocommerce_currency_pos', - 'css' => 'min-width:350px;', - 'class' => 'chosen_select', - 'default' => 'left', - 'type' => 'select', - 'options' => array( + 'title' => __( 'Store notice', 'woocommerce' ), + 'desc' => __( 'Enable site-wide store notice text', 'woocommerce' ), + 'id' => 'woocommerce_demo_store', + 'default' => 'no', + 'type' => 'checkbox', + ), + + array( + 'title' => __( 'Store notice text', 'woocommerce' ), + 'desc' => '', + 'id' => 'woocommerce_demo_store_notice', + 'default' => __( 'This is a demo store for testing purposes — no orders shall be fulfilled.', 'woocommerce' ), + 'type' => 'textarea', + 'css' => 'width:350px; height: 65px;', + 'autoload' => false, + ), + + array( 'type' => 'sectionend', 'id' => 'general_options' ), + + array( 'title' => __( 'Currency options', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'The following options affect how prices are displayed on the frontend.', 'woocommerce' ), 'id' => 'pricing_options' ), + + array( + 'title' => __( 'Currency', 'woocommerce' ), + 'desc' => __( 'This controls what currency prices are listed at in the catalog and which currency gateways will take payments in.', 'woocommerce' ), + 'id' => 'woocommerce_currency', + 'default' => 'GBP', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'desc_tip' => true, + 'options' => $currency_code_options, + ), + + array( + 'title' => __( 'Currency position', 'woocommerce' ), + 'desc' => __( 'This controls the position of the currency symbol.', 'woocommerce' ), + 'id' => 'woocommerce_currency_pos', + 'class' => 'wc-enhanced-select', + 'default' => 'left', + 'type' => 'select', + 'options' => array( 'left' => __( 'Left', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . '99.99)', 'right' => __( 'Right', 'woocommerce' ) . ' (99.99' . get_woocommerce_currency_symbol() . ')', 'left_space' => __( 'Left with space', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ' 99.99)', - 'right_space' => __( 'Right with space', 'woocommerce' ) . ' (99.99 ' . get_woocommerce_currency_symbol() . ')' + 'right_space' => __( 'Right with space', 'woocommerce' ) . ' (99.99 ' . get_woocommerce_currency_symbol() . ')', ), - 'desc_tip' => true, + 'desc_tip' => true, ), array( - 'title' => __( 'Thousand Separator', 'woocommerce' ), - 'desc' => __( 'This sets the thousand separator of displayed prices.', 'woocommerce' ), - 'id' => 'woocommerce_price_thousand_sep', - 'css' => 'width:50px;', - 'default' => ',', - 'type' => 'text', - 'desc_tip' => true, + 'title' => __( 'Thousand separator', 'woocommerce' ), + 'desc' => __( 'This sets the thousand separator of displayed prices.', 'woocommerce' ), + 'id' => 'woocommerce_price_thousand_sep', + 'css' => 'width:50px;', + 'default' => ',', + 'type' => 'text', + 'desc_tip' => true, ), array( - 'title' => __( 'Decimal Separator', 'woocommerce' ), - 'desc' => __( 'This sets the decimal separator of displayed prices.', 'woocommerce' ), - 'id' => 'woocommerce_price_decimal_sep', - 'css' => 'width:50px;', - 'default' => '.', - 'type' => 'text', - 'desc_tip' => true, + 'title' => __( 'Decimal separator', 'woocommerce' ), + 'desc' => __( 'This sets the decimal separator of displayed prices.', 'woocommerce' ), + 'id' => 'woocommerce_price_decimal_sep', + 'css' => 'width:50px;', + 'default' => '.', + 'type' => 'text', + 'desc_tip' => true, ), array( - 'title' => __( 'Number of Decimals', 'woocommerce' ), - 'desc' => __( 'This sets the number of decimal points shown in displayed prices.', 'woocommerce' ), - 'id' => 'woocommerce_price_num_decimals', - 'css' => 'width:50px;', - 'default' => '2', - 'desc_tip' => true, - 'type' => 'number', + 'title' => __( 'Number of decimals', 'woocommerce' ), + 'desc' => __( 'This sets the number of decimal points shown in displayed prices.', 'woocommerce' ), + 'id' => 'woocommerce_price_num_decimals', + 'css' => 'width:50px;', + 'default' => '2', + 'desc_tip' => true, + 'type' => 'number', 'custom_attributes' => array( - 'min' => 0, - 'step' => 1 - ) + 'min' => 0, + 'step' => 1, + ), ), array( 'type' => 'sectionend', 'id' => 'pricing_options' ), - array( 'title' => __( 'Styles and Scripts', 'woocommerce' ), 'type' => 'title', 'id' => 'script_styling_options' ), + ) ); - array( 'type' => 'frontend_styles' ), - - array( - 'title' => __( 'Scripts', 'woocommerce' ), - 'desc' => __( 'Enable Lightbox', 'woocommerce' ), - 'id' => 'woocommerce_enable_lightbox', - 'default' => 'yes', - 'desc_tip' => __( 'Include WooCommerce\'s lightbox. Product gallery images will open in a lightbox.', 'woocommerce' ), - 'type' => 'checkbox', - 'checkboxgroup' => 'start' - ), - - array( - 'desc' => __( 'Enable enhanced country select boxes', 'woocommerce' ), - 'id' => 'woocommerce_enable_chosen', - 'default' => 'yes', - 'type' => 'checkbox', - 'checkboxgroup' => 'end', - 'desc_tip' => __( 'This will enable a script allowing the country fields to be searchable.', 'woocommerce' ), - 'autoload' => false - ), - - array( 'type' => 'sectionend', 'id' => 'script_styling_options' ), - - ) ); // End general settings + return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings ); } /** - * Output the frontend styles settings. + * Output a color picker input box. * - * @access public - * @return void - */ - public function frontend_styles_setting() { - ?> - - - - plugin_path() . '/assets/css/woocommerce-base.less'; - $css_file = WC()->plugin_path() . '/assets/css/woocommerce.css'; - - if ( is_writable( $base_file ) && is_writable( $css_file ) ) { - - // Get settings - $colors = array_map( 'esc_attr', (array) get_option( 'woocommerce_frontend_css_colors' ) ); - - // Defaults - if ( empty( $colors['primary'] ) ) { - $colors['primary'] = '#ad74a2'; - } - if ( empty( $colors['secondary'] ) ) { - $colors['secondary'] = '#f7f6f7'; - } - if ( empty( $colors['highlight'] ) ) { - $colors['highlight'] = '#85ad74'; - } - if ( empty( $colors['content_bg'] ) ) { - $colors['content_bg'] = '#ffffff'; - } - if ( empty( $colors['subtext'] ) ) { - $colors['subtext'] = '#777777'; - } - - // Show inputs - $this->color_picker( __( 'Primary', 'woocommerce' ), 'woocommerce_frontend_css_primary', $colors['primary'], __( 'Call to action buttons/price slider/layered nav UI', 'woocommerce' ) ); - $this->color_picker( __( 'Secondary', 'woocommerce' ), 'woocommerce_frontend_css_secondary', $colors['secondary'], __( 'Buttons and tabs', 'woocommerce' ) ); - $this->color_picker( __( 'Highlight', 'woocommerce' ), 'woocommerce_frontend_css_highlight', $colors['highlight'], __( 'Price labels and Sale Flashes', 'woocommerce' ) ); - $this->color_picker( __( 'Content', 'woocommerce' ), 'woocommerce_frontend_css_content_bg', $colors['content_bg'], __( 'Your themes page background - used for tab active states', 'woocommerce' ) ); - $this->color_picker( __( 'Subtext', 'woocommerce' ), 'woocommerce_frontend_css_subtext', $colors['subtext'], __( 'Used for certain text and asides - breadcrumbs, small text etc.', 'woocommerce' ) ); - - } else { - echo '' . __( 'To edit colours woocommerce/assets/css/woocommerce-base.less and woocommerce.css need to be writable. See the Codex for more information.', 'woocommerce' ) . ''; - } - - ?> - ' . esc_html( $name ) . ' -
    + public function color_picker( $name, $id, $value, $desc = '' ) { + echo '
    ' . wc_help_tip( $desc ) . ' +
    '; } /** - * Save settings + * Save settings. */ public function save() { $settings = $this->get_settings(); WC_Admin_Settings::save_fields( $settings ); - - if ( isset( $_POST['woocommerce_frontend_css_primary'] ) ) { - - // Save settings - $primary = ( ! empty( $_POST['woocommerce_frontend_css_primary'] ) ) ? wc_format_hex( $_POST['woocommerce_frontend_css_primary'] ) : ''; - $secondary = ( ! empty( $_POST['woocommerce_frontend_css_secondary'] ) ) ? wc_format_hex( $_POST['woocommerce_frontend_css_secondary'] ) : ''; - $highlight = ( ! empty( $_POST['woocommerce_frontend_css_highlight'] ) ) ? wc_format_hex( $_POST['woocommerce_frontend_css_highlight'] ) : ''; - $content_bg = ( ! empty( $_POST['woocommerce_frontend_css_content_bg'] ) ) ? wc_format_hex( $_POST['woocommerce_frontend_css_content_bg'] ) : ''; - $subtext = ( ! empty( $_POST['woocommerce_frontend_css_subtext'] ) ) ? wc_format_hex( $_POST['woocommerce_frontend_css_subtext'] ) : ''; - - $colors = array( - 'primary' => $primary, - 'secondary' => $secondary, - 'highlight' => $highlight, - 'content_bg' => $content_bg, - 'subtext' => $subtext - ); - - // Check the colors. - $valid_colors = true; - foreach ( $colors as $color ) { - if ( ! preg_match( '/^#[a-f0-9]{6}$/i', $color ) ) { - $valid_colors = false; - WC_Admin_Settings::add_error( sprintf( __( 'Error saving the Frontend Styles, %s is not a valid color, please use only valid colors code.', 'woocommerce' ), $color ) ); - break; - } - } - - if ( $valid_colors ) { - $old_colors = get_option( 'woocommerce_frontend_css_colors' ); - update_option( 'woocommerce_frontend_css_colors', $colors ); - - if ( $old_colors != $colors ) { - woocommerce_compile_less_styles(); - } - } - } } - } endif; diff --git a/includes/admin/settings/class-wc-settings-integrations.php b/includes/admin/settings/class-wc-settings-integrations.php index d1c5febabe6..126844c85f6 100644 --- a/includes/admin/settings/class-wc-settings-integrations.php +++ b/includes/admin/settings/class-wc-settings-integrations.php @@ -2,18 +2,20 @@ /** * WooCommerce Integration Settings * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} -if ( ! class_exists( 'WC_Settings_Integrations' ) ) : +if ( ! class_exists( 'WC_Settings_Integrations', false ) ) : /** - * WC_Settings_Integrations + * WC_Settings_Integrations. */ class WC_Settings_Integrations extends WC_Settings_Page { @@ -21,6 +23,7 @@ class WC_Settings_Integrations extends WC_Settings_Page { * Constructor. */ public function __construct() { + $this->id = 'integration'; $this->label = __( 'Integration', 'woocommerce' ); @@ -33,7 +36,7 @@ class WC_Settings_Integrations extends WC_Settings_Page { } /** - * Get sections + * Get sections. * * @return array */ @@ -42,30 +45,35 @@ class WC_Settings_Integrations extends WC_Settings_Page { $sections = array(); - $integrations = WC()->integrations->get_integrations(); + if ( ! defined( 'WC_INSTALLING' ) ) { + $integrations = WC()->integrations->get_integrations(); - if ( ! $current_section && ! empty( $integrations ) ) - $current_section = current( $integrations )->id; + if ( ! $current_section && ! empty( $integrations ) ) { + $current_section = current( $integrations )->id; + } - foreach ( $integrations as $integration ) { - $title = empty( $integration->method_title ) ? ucfirst( $integration->id ) : $integration->method_title; - - $sections[ strtolower( $integration->id ) ] = esc_html( $title ); + if ( sizeof( $integrations ) > 1 ) { + foreach ( $integrations as $integration ) { + $title = empty( $integration->method_title ) ? ucfirst( $integration->id ) : $integration->method_title; + $sections[ strtolower( $integration->id ) ] = esc_html( $title ); + } + } } return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); } /** - * Output the settings + * Output the settings. */ public function output() { global $current_section; $integrations = WC()->integrations->get_integrations(); - if ( isset( $integrations[ $current_section ] ) ) + if ( isset( $integrations[ $current_section ] ) ) { $integrations[ $current_section ]->admin_options(); + } } } diff --git a/includes/admin/settings/class-wc-settings-page.php b/includes/admin/settings/class-wc-settings-page.php index 4635a606d72..830fa25e030 100644 --- a/includes/admin/settings/class-wc-settings-page.php +++ b/includes/admin/settings/class-wc-settings-page.php @@ -2,26 +2,71 @@ /** * WooCommerce Settings Page/Tab * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} -if ( ! class_exists( 'WC_Settings_Page' ) ) : +if ( ! class_exists( 'WC_Settings_Page', false ) ) : /** - * WC_Settings_Page + * WC_Settings_Page. */ -class WC_Settings_Page { +abstract class WC_Settings_Page { - protected $id = ''; + /** + * Setting page id. + * + * @var string + */ + protected $id = ''; + + /** + * Setting page label. + * + * @var string + */ protected $label = ''; /** - * Add this page to settings + * Constructor. + */ + public function __construct() { + add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 ); + add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) ); + add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) ); + add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) ); + } + + /** + * Get settings page ID. + * @since 3.0.0 + * @return string + */ + public function get_id() { + return $this->id; + } + + /** + * Get settings page label. + * @since 3.0.0 + * @return string + */ + public function get_label() { + return $this->label; + } + + /** + * Add this page to settings. + * + * @param array $pages + * + * @return mixed */ public function add_settings_page( $pages ) { $pages[ $this->id ] = $this->label; @@ -30,16 +75,16 @@ class WC_Settings_Page { } /** - * Get settings array + * Get settings array. * * @return array */ public function get_settings() { - return array(); + return apply_filters( 'woocommerce_get_settings_' . $this->id, array() ); } /** - * Get sections + * Get sections. * * @return array */ @@ -48,28 +93,30 @@ class WC_Settings_Page { } /** - * Output sections + * Output sections. */ public function output_sections() { global $current_section; $sections = $this->get_sections(); - if ( empty( $sections ) ) + if ( empty( $sections ) || 1 === sizeof( $sections ) ) { return; + } echo '
      '; $array_keys = array_keys( $sections ); - foreach ( $sections as $id => $label ) + foreach ( $sections as $id => $label ) { echo '
    • ' . $label . ' ' . ( end( $array_keys ) == $id ? '' : '|' ) . '
    • '; + } echo '

    '; } /** - * Output the settings + * Output the settings. */ public function output() { $settings = $this->get_settings(); @@ -78,7 +125,7 @@ class WC_Settings_Page { } /** - * Save settings + * Save settings. */ public function save() { global $current_section; @@ -86,9 +133,10 @@ class WC_Settings_Page { $settings = $this->get_settings(); WC_Admin_Settings::save_fields( $settings ); - if ( $current_section ) - do_action( 'woocommerce_update_options_' . $this->id . '_' . $current_section ); + if ( $current_section ) { + do_action( 'woocommerce_update_options_' . $this->id . '_' . $current_section ); + } } } -endif; \ No newline at end of file +endif; diff --git a/includes/admin/settings/class-wc-settings-products.php b/includes/admin/settings/class-wc-settings-products.php index c20ef48241b..eecdd0defb6 100644 --- a/includes/admin/settings/class-wc-settings-products.php +++ b/includes/admin/settings/class-wc-settings-products.php @@ -2,18 +2,20 @@ /** * WooCommerce Product Settings * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin - * @version 2.1.0 + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin + * @version 2.4.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} -if ( ! class_exists( 'WC_Settings_Products' ) ) : +if ( ! class_exists( 'WC_Settings_Products', false ) ) : /** - * WC_Settings_Products + * WC_Settings_Products. */ class WC_Settings_Products extends WC_Settings_Page { @@ -21,6 +23,7 @@ class WC_Settings_Products extends WC_Settings_Page { * Constructor. */ public function __construct() { + $this->id = 'products'; $this->label = __( 'Products', 'woocommerce' ); @@ -31,32 +34,35 @@ class WC_Settings_Products extends WC_Settings_Page { } /** - * Get sections + * Get sections. * * @return array */ public function get_sections() { + $sections = array( - '' => __( 'Product Options', 'woocommerce' ), - 'inventory' => __( 'Inventory', 'woocommerce' ) + '' => __( 'General', 'woocommerce' ), + 'display' => __( 'Display', 'woocommerce' ), + 'inventory' => __( 'Inventory', 'woocommerce' ), + 'downloadable' => __( 'Downloadable products', 'woocommerce' ), ); return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); } /** - * Output the settings + * Output the settings. */ public function output() { global $current_section; $settings = $this->get_settings( $current_section ); - WC_Admin_Settings::output_fields( $settings ); + WC_Admin_Settings::output_fields( $settings ); } /** - * Save settings + * Save settings. */ public function save() { global $current_section; @@ -66,393 +72,462 @@ class WC_Settings_Products extends WC_Settings_Page { } /** - * Get settings array + * Get settings array. + * + * @param string $current_section * * @return array */ public function get_settings( $current_section = '' ) { + if ( 'display' == $current_section ) { - if ( $current_section == 'inventory' ) { - - return apply_filters('woocommerce_inventory_settings', array( - - array( 'title' => __( 'Inventory Options', 'woocommerce' ), 'type' => 'title', 'desc' => '', 'id' => 'inventory_options' ), + $settings = apply_filters( 'woocommerce_product_settings', array( array( - 'title' => __( 'Manage Stock', 'woocommerce' ), - 'desc' => __( 'Enable stock management', 'woocommerce' ), - 'id' => 'woocommerce_manage_stock', - 'default' => 'yes', - 'type' => 'checkbox' + 'title' => __( 'Shop & product pages', 'woocommerce' ), + 'type' => 'title', + 'desc' => '', + 'id' => 'catalog_options', ), array( - 'title' => __( 'Hold Stock (minutes)', 'woocommerce' ), - 'desc' => __( 'Hold stock (for unpaid orders) for x minutes. When this limit is reached, the pending order will be cancelled. Leave blank to disable.', 'woocommerce' ), - 'id' => 'woocommerce_hold_stock_minutes', - 'type' => 'number', - 'custom_attributes' => array( - 'min' => 0, - 'step' => 1 + 'title' => __( 'Shop page', 'woocommerce' ), + 'desc' => '
    ' . sprintf( __( 'The base page can also be used in your product permalinks.', 'woocommerce' ), admin_url( 'options-permalink.php' ) ), + 'id' => 'woocommerce_shop_page_id', + 'type' => 'single_select_page', + 'default' => '', + 'class' => 'wc-enhanced-select-nostd', + 'css' => 'min-width:300px;', + 'desc_tip' => __( 'This sets the base page of your shop - this is where your product archive will be.', 'woocommerce' ), + ), + + array( + 'title' => __( 'Shop page display', 'woocommerce' ), + 'desc' => __( 'This controls what is shown on the product archive.', 'woocommerce' ), + 'id' => 'woocommerce_shop_page_display', + 'class' => 'wc-enhanced-select', + 'css' => 'min-width:300px;', + 'default' => '', + 'type' => 'select', + 'options' => array( + '' => __( 'Show products', 'woocommerce' ), + 'subcategories' => __( 'Show categories', 'woocommerce' ), + 'both' => __( 'Show categories & products', 'woocommerce' ), ), - 'css' => 'width:50px;', - 'default' => '60', - 'autoload' => false + 'desc_tip' => true, ), array( - 'title' => __( 'Notifications', 'woocommerce' ), - 'desc' => __( 'Enable low stock notifications', 'woocommerce' ), - 'id' => 'woocommerce_notify_low_stock', - 'default' => 'yes', - 'type' => 'checkbox', + 'title' => __( 'Default category display', 'woocommerce' ), + 'desc' => __( 'This controls what is shown on category archives.', 'woocommerce' ), + 'id' => 'woocommerce_category_archive_display', + 'class' => 'wc-enhanced-select', + 'css' => 'min-width:300px;', + 'default' => '', + 'type' => 'select', + 'options' => array( + '' => __( 'Show products', 'woocommerce' ), + 'subcategories' => __( 'Show subcategories', 'woocommerce' ), + 'both' => __( 'Show subcategories & products', 'woocommerce' ), + ), + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Default product sorting', 'woocommerce' ), + 'desc' => __( 'This controls the default sort order of the catalog.', 'woocommerce' ), + 'id' => 'woocommerce_default_catalog_orderby', + 'class' => 'wc-enhanced-select', + 'css' => 'min-width:300px;', + 'default' => 'menu_order', + 'type' => 'select', + 'options' => apply_filters( 'woocommerce_default_catalog_orderby_options', array( + 'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ), + 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), + 'rating' => __( 'Average rating', 'woocommerce' ), + 'date' => __( 'Sort by most recent', 'woocommerce' ), + 'price' => __( 'Sort by price (asc)', 'woocommerce' ), + 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), + ) ), + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Add to cart behaviour', 'woocommerce' ), + 'desc' => __( 'Redirect to the cart page after successful addition', 'woocommerce' ), + 'id' => 'woocommerce_cart_redirect_after_add', + 'default' => 'no', + 'type' => 'checkbox', 'checkboxgroup' => 'start', - 'autoload' => false ), array( - 'desc' => __( 'Enable out of stock notifications', 'woocommerce' ), - 'id' => 'woocommerce_notify_no_stock', - 'default' => 'yes', - 'type' => 'checkbox', + 'desc' => __( 'Enable AJAX add to cart buttons on archives', 'woocommerce' ), + 'id' => 'woocommerce_enable_ajax_add_to_cart', + 'default' => 'yes', + 'type' => 'checkbox', 'checkboxgroup' => 'end', - 'autoload' => false ), array( - 'title' => __( 'Notification Recipient', 'woocommerce' ), - 'desc' => '', - 'id' => 'woocommerce_stock_email_recipient', - 'type' => 'email', - 'default' => get_option( 'admin_email' ), - 'autoload' => false + 'type' => 'sectionend', + 'id' => 'catalog_options', ), array( - 'title' => __( 'Low Stock Threshold', 'woocommerce' ), - 'desc' => '', - 'id' => 'woocommerce_notify_low_stock_amount', - 'css' => 'width:50px;', - 'type' => 'number', + 'title' => __( 'Product images', 'woocommerce' ), + 'type' => 'title', + 'desc' => sprintf( __( 'These settings affect the display and dimensions of images in your catalog - the display on the front-end will still be affected by CSS styles. After changing these settings you may need to regenerate your thumbnails.', 'woocommerce' ), 'https://wordpress.org/plugins/regenerate-thumbnails/' ), + 'id' => 'image_options', + ), + + array( + 'title' => __( 'Catalog images', 'woocommerce' ), + 'desc' => __( 'This size is usually used in product listings. (W x H)', 'woocommerce' ), + 'id' => 'shop_catalog_image_size', + 'css' => '', + 'type' => 'image_width', + 'default' => array( + 'width' => '300', + 'height' => '300', + 'crop' => 1, + ), + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Single product image', 'woocommerce' ), + 'desc' => __( 'This is the size used by the main image on the product page. (W x H)', 'woocommerce' ), + 'id' => 'shop_single_image_size', + 'css' => '', + 'type' => 'image_width', + 'default' => array( + 'width' => '600', + 'height' => '600', + 'crop' => 1, + ), + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Product thumbnails', 'woocommerce' ), + 'desc' => __( 'This size is usually used for the gallery of images on the product page. (W x H)', 'woocommerce' ), + 'id' => 'shop_thumbnail_image_size', + 'css' => '', + 'type' => 'image_width', + 'default' => array( + 'width' => '180', + 'height' => '180', + 'crop' => 1, + ), + 'desc_tip' => true, + ), + + array( + 'type' => 'sectionend', + 'id' => 'image_options', + ), + + )); + } elseif ( 'inventory' == $current_section ) { + + $settings = apply_filters( 'woocommerce_inventory_settings', array( + + array( + 'title' => __( 'Inventory', 'woocommerce' ), + 'type' => 'title', + 'desc' => '', + 'id' => 'product_inventory_options', + ), + + array( + 'title' => __( 'Manage stock', 'woocommerce' ), + 'desc' => __( 'Enable stock management', 'woocommerce' ), + 'id' => 'woocommerce_manage_stock', + 'default' => 'yes', + 'type' => 'checkbox', + ), + + array( + 'title' => __( 'Hold stock (minutes)', 'woocommerce' ), + 'desc' => __( 'Hold stock (for unpaid orders) for x minutes. When this limit is reached, the pending order will be cancelled. Leave blank to disable.', 'woocommerce' ), + 'id' => 'woocommerce_hold_stock_minutes', + 'type' => 'number', 'custom_attributes' => array( - 'min' => 0, - 'step' => 1 + 'min' => 0, + 'step' => 1, ), - 'default' => '2', - 'autoload' => false + 'css' => 'width: 80px;', + 'default' => '60', + 'autoload' => false, + 'class' => 'manage_stock_field', ), array( - 'title' => __( 'Out Of Stock Threshold', 'woocommerce' ), - 'desc' => '', - 'id' => 'woocommerce_notify_no_stock_amount', - 'css' => 'width:50px;', - 'type' => 'number', + 'title' => __( 'Notifications', 'woocommerce' ), + 'desc' => __( 'Enable low stock notifications', 'woocommerce' ), + 'id' => 'woocommerce_notify_low_stock', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + 'autoload' => false, + 'class' => 'manage_stock_field', + ), + + array( + 'desc' => __( 'Enable out of stock notifications', 'woocommerce' ), + 'id' => 'woocommerce_notify_no_stock', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => 'end', + 'autoload' => false, + 'class' => 'manage_stock_field', + ), + + array( + 'title' => __( 'Notification recipient(s)', 'woocommerce' ), + 'desc' => __( 'Enter recipients (comma separated) that will receive this notification.', 'woocommerce' ), + 'id' => 'woocommerce_stock_email_recipient', + 'type' => 'text', + 'default' => get_option( 'admin_email' ), + 'css' => 'width: 250px;', + 'autoload' => false, + 'desc_tip' => true, + 'class' => 'manage_stock_field', + ), + + array( + 'title' => __( 'Low stock threshold', 'woocommerce' ), + 'desc' => __( 'When product stock reaches this amount you will be notified via email.', 'woocommerce' ), + 'id' => 'woocommerce_notify_low_stock_amount', + 'css' => 'width:50px;', + 'type' => 'number', 'custom_attributes' => array( - 'min' => 0, - 'step' => 1 + 'min' => 0, + 'step' => 1, ), - 'default' => '0', - 'autoload' => false + 'default' => '2', + 'autoload' => false, + 'desc_tip' => true, + 'class' => 'manage_stock_field', ), array( - 'title' => __( 'Out Of Stock Visibility', 'woocommerce' ), - 'desc' => __( 'Hide out of stock items from the catalog', 'woocommerce' ), - 'id' => 'woocommerce_hide_out_of_stock_items', - 'default' => 'no', - 'type' => 'checkbox' + 'title' => __( 'Out of stock threshold', 'woocommerce' ), + 'desc' => __( 'When product stock reaches this amount the stock status will change to "out of stock" and you will be notified via email. This setting does not affect existing "in stock" products.', 'woocommerce' ), + 'id' => 'woocommerce_notify_no_stock_amount', + 'css' => 'width:50px;', + 'type' => 'number', + 'custom_attributes' => array( + 'min' => 0, + 'step' => 1, + ), + 'default' => '0', + 'desc_tip' => true, + 'class' => 'manage_stock_field', ), array( - 'title' => __( 'Stock Display Format', 'woocommerce' ), - 'desc' => __( 'This controls how stock is displayed on the frontend.', 'woocommerce' ), - 'id' => 'woocommerce_stock_format', - 'css' => 'min-width:150px;', - 'default' => '', - 'type' => 'select', - 'options' => array( - '' => __( 'Always show stock e.g. "12 in stock"', 'woocommerce' ), - 'low_amount' => __( 'Only show stock when low e.g. "Only 2 left in stock" vs. "In Stock"', 'woocommerce' ), - 'no_amount' => __( 'Never show stock amount', 'woocommerce' ), - ), - 'desc_tip' => true, + 'title' => __( 'Out of stock visibility', 'woocommerce' ), + 'desc' => __( 'Hide out of stock items from the catalog', 'woocommerce' ), + 'id' => 'woocommerce_hide_out_of_stock_items', + 'default' => 'no', + 'type' => 'checkbox', ), - array( 'type' => 'sectionend', 'id' => 'inventory_options'), + array( + 'title' => __( 'Stock display format', 'woocommerce' ), + 'desc' => __( 'This controls how stock quantities are displayed on the frontend.', 'woocommerce' ), + 'id' => 'woocommerce_stock_format', + 'css' => 'min-width:150px;', + 'class' => 'wc-enhanced-select', + 'default' => '', + 'type' => 'select', + 'options' => array( + '' => __( 'Always show quantity remaining in stock e.g. "12 in stock"', 'woocommerce' ), + 'low_amount' => __( 'Only show quantity remaining in stock when low e.g. "Only 2 left in stock"', 'woocommerce' ), + 'no_amount' => __( 'Never show quantity remaining in stock', 'woocommerce' ), + ), + 'desc_tip' => true, + ), + + array( + 'type' => 'sectionend', + 'id' => 'product_inventory_options', + ), + + )); + + } elseif ( 'downloadable' == $current_section ) { + $settings = apply_filters( 'woocommerce_downloadable_products_settings', array( + array( + 'title' => __( 'Downloadable products', 'woocommerce' ), + 'type' => 'title', + 'id' => 'digital_download_options', + ), + + array( + 'title' => __( 'File download method', 'woocommerce' ), + 'desc' => __( 'Forcing downloads will keep URLs hidden, but some servers may serve large files unreliably. If supported, X-Accel-Redirect/ X-Sendfile can be used to serve downloads instead (server requires mod_xsendfile).', 'woocommerce' ), + 'id' => 'woocommerce_file_download_method', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'css' => 'min-width:300px;', + 'default' => 'force', + 'desc_tip' => true, + 'options' => array( + 'force' => __( 'Force downloads', 'woocommerce' ), + 'xsendfile' => __( 'X-Accel-Redirect/X-Sendfile', 'woocommerce' ), + 'redirect' => __( 'Redirect only', 'woocommerce' ), + ), + 'autoload' => false, + ), + + array( + 'title' => __( 'Access restriction', 'woocommerce' ), + 'desc' => __( 'Downloads require login', 'woocommerce' ), + 'id' => 'woocommerce_downloads_require_login', + 'type' => 'checkbox', + 'default' => 'no', + 'desc_tip' => __( 'This setting does not apply to guest purchases.', 'woocommerce' ), + 'checkboxgroup' => 'start', + 'autoload' => false, + ), + + array( + 'desc' => __( 'Grant access to downloadable products after payment', 'woocommerce' ), + 'id' => 'woocommerce_downloads_grant_access_after_payment', + 'type' => 'checkbox', + 'default' => 'yes', + 'desc_tip' => __( 'Enable this option to grant access to downloads when orders are "processing", rather than "completed".', 'woocommerce' ), + 'checkboxgroup' => 'end', + 'autoload' => false, + ), + + array( + 'type' => 'sectionend', + 'id' => 'digital_download_options', + ), )); } else { - - // Get shop page - $shop_page_id = wc_get_page_id('shop'); - - $base_slug = ($shop_page_id > 0 && get_page( $shop_page_id )) ? get_page_uri( $shop_page_id ) : 'shop'; - - $woocommerce_prepend_shop_page_to_products_warning = ''; - - if ( $shop_page_id > 0 && sizeof(get_pages("child_of=$shop_page_id")) > 0 ) - $woocommerce_prepend_shop_page_to_products_warning = ' ' . __( 'Note: The shop page has children - child pages will not work if you enable this option.', 'woocommerce' ) . ''; - - return apply_filters( 'woocommerce_product_settings', array( - - array( 'title' => __( 'Product Listings', 'woocommerce' ), 'type' => 'title','desc' => '', 'id' => 'catalog_options' ), - + $settings = apply_filters( 'woocommerce_products_general_settings', array( array( - 'title' => __( 'Product Archive / Shop Page', 'woocommerce' ), - 'desc' => '
    ' . sprintf( __( 'The base page can also be used in your product permalinks.', 'woocommerce' ), admin_url( 'options-permalink.php' ) ), - 'id' => 'woocommerce_shop_page_id', - 'type' => 'single_select_page', - 'default' => '', - 'class' => 'chosen_select_nostd', - 'css' => 'min-width:300px;', - 'desc_tip' => __( 'This sets the base page of your shop - this is where your product archive will be.', 'woocommerce' ), + 'title' => __( 'Measurements', 'woocommerce' ), + 'type' => 'title', + 'id' => 'product_measurement_options', ), array( - 'title' => __( 'Shop Page Display', 'woocommerce' ), - 'desc' => __( 'This controls what is shown on the product archive.', 'woocommerce' ), - 'id' => 'woocommerce_shop_page_display', - 'class' => 'chosen_select', - 'css' => 'min-width:300px;', - 'default' => '', - 'type' => 'select', - 'options' => array( - '' => __( 'Show products', 'woocommerce' ), - 'subcategories' => __( 'Show subcategories', 'woocommerce' ), - 'both' => __( 'Show both', 'woocommerce' ), - ), - 'desc_tip' => true, - ), - - array( - 'title' => __( 'Default Category Display', 'woocommerce' ), - 'desc' => __( 'This controls what is shown on category archives.', 'woocommerce' ), - 'id' => 'woocommerce_category_archive_display', - 'class' => 'chosen_select', - 'css' => 'min-width:300px;', - 'default' => '', - 'type' => 'select', - 'options' => array( - '' => __( 'Show products', 'woocommerce' ), - 'subcategories' => __( 'Show subcategories', 'woocommerce' ), - 'both' => __( 'Show both', 'woocommerce' ), - ), - 'desc_tip' => true, - ), - - array( - 'title' => __( 'Default Product Sorting', 'woocommerce' ), - 'desc' => __( 'This controls the default sort order of the catalog.', 'woocommerce' ), - 'id' => 'woocommerce_default_catalog_orderby', - 'class' => 'chosen_select', - 'css' => 'min-width:300px;', - 'default' => 'title', - 'type' => 'select', - 'options' => apply_filters('woocommerce_default_catalog_orderby_options', array( - 'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ), - 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), - 'rating' => __( 'Average Rating', 'woocommerce' ), - 'date' => __( 'Sort by most recent', 'woocommerce' ), - 'price' => __( 'Sort by price (asc)', 'woocommerce' ), - 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), - )), - 'desc_tip' => true, - ), - - array( - 'title' => __( 'Add to cart', 'woocommerce' ), - 'desc' => __( 'Redirect to the cart page after successful addition', 'woocommerce' ), - 'id' => 'woocommerce_cart_redirect_after_add', - 'default' => 'no', - 'type' => 'checkbox', - 'checkboxgroup' => 'start' - ), - - array( - 'desc' => __( 'Enable AJAX add to cart buttons on archives', 'woocommerce' ), - 'id' => 'woocommerce_enable_ajax_add_to_cart', - 'default' => 'yes', - 'type' => 'checkbox', - 'checkboxgroup' => 'end' - ), - - array( 'type' => 'sectionend', 'id' => 'catalog_options' ), - - array( 'title' => __( 'Product Data', 'woocommerce' ), 'type' => 'title', 'id' => 'product_data_options' ), - - array( - 'title' => __( 'Weight Unit', 'woocommerce' ), - 'desc' => __( 'This controls what unit you will define weights in.', 'woocommerce' ), - 'id' => 'woocommerce_weight_unit', - 'class' => 'chosen_select', - 'css' => 'min-width:300px;', - 'default' => 'kg', - 'type' => 'select', - 'options' => array( + 'title' => __( 'Weight unit', 'woocommerce' ), + 'desc' => __( 'This controls what unit you will define weights in.', 'woocommerce' ), + 'id' => 'woocommerce_weight_unit', + 'class' => 'wc-enhanced-select', + 'css' => 'min-width:300px;', + 'default' => 'kg', + 'type' => 'select', + 'options' => array( 'kg' => __( 'kg', 'woocommerce' ), 'g' => __( 'g', 'woocommerce' ), 'lbs' => __( 'lbs', 'woocommerce' ), - 'oz' => __( 'oz', 'woocommerce' ), + 'oz' => __( 'oz', 'woocommerce' ), ), - 'desc_tip' => true, + 'desc_tip' => true, ), array( - 'title' => __( 'Dimensions Unit', 'woocommerce' ), - 'desc' => __( 'This controls what unit you will define lengths in.', 'woocommerce' ), - 'id' => 'woocommerce_dimension_unit', - 'class' => 'chosen_select', - 'css' => 'min-width:300px;', - 'default' => 'cm', - 'type' => 'select', - 'options' => array( + 'title' => __( 'Dimensions unit', 'woocommerce' ), + 'desc' => __( 'This controls what unit you will define lengths in.', 'woocommerce' ), + 'id' => 'woocommerce_dimension_unit', + 'class' => 'wc-enhanced-select', + 'css' => 'min-width:300px;', + 'default' => 'cm', + 'type' => 'select', + 'options' => array( 'm' => __( 'm', 'woocommerce' ), 'cm' => __( 'cm', 'woocommerce' ), 'mm' => __( 'mm', 'woocommerce' ), 'in' => __( 'in', 'woocommerce' ), 'yd' => __( 'yd', 'woocommerce' ), ), - 'desc_tip' => true, + 'desc_tip' => true, ), array( - 'title' => __( 'Product Ratings', 'woocommerce' ), - 'desc' => __( 'Enable ratings on reviews', 'woocommerce' ), - 'id' => 'woocommerce_enable_review_rating', - 'default' => 'yes', - 'type' => 'checkbox', - 'checkboxgroup' => 'start', + 'type' => 'sectionend', + 'id' => 'product_measurement_options', + ), + + array( + 'title' => __( 'Reviews', 'woocommerce' ), + 'type' => 'title', + 'desc' => '', + 'id' => 'product_rating_options', + ), + + array( + 'title' => __( 'Enable reviews', 'woocommerce' ), + 'desc' => __( 'Enable product reviews', 'woocommerce' ), + 'id' => 'woocommerce_enable_reviews', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', 'show_if_checked' => 'option', - 'autoload' => false ), array( - 'desc' => __( 'Ratings are required to leave a review', 'woocommerce' ), - 'id' => 'woocommerce_review_rating_required', - 'default' => 'yes', - 'type' => 'checkbox', - 'checkboxgroup' => '', + 'desc' => __( 'Show "verified owner" label on customer reviews', 'woocommerce' ), + 'id' => 'woocommerce_review_rating_verification_label', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => '', 'show_if_checked' => 'yes', - 'autoload' => false + 'autoload' => false, ), array( - 'desc' => __( 'Show "verified owner" label for customer reviews', 'woocommerce' ), - 'id' => 'woocommerce_review_rating_verification_label', - 'default' => 'yes', - 'type' => 'checkbox', - 'checkboxgroup' => '', + 'desc' => __( 'Reviews can only be left by "verified owners"', 'woocommerce' ), + 'id' => 'woocommerce_review_rating_verification_required', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'end', 'show_if_checked' => 'yes', - 'autoload' => false + 'autoload' => false, ), array( - 'desc' => __( 'Only allow reviews from "verified owners"', 'woocommerce' ), - 'id' => 'woocommerce_review_rating_verification_required', - 'default' => 'no', - 'type' => 'checkbox', - 'checkboxgroup' => 'end', + 'title' => __( 'Product ratings', 'woocommerce' ), + 'desc' => __( 'Enable star rating on reviews', 'woocommerce' ), + 'id' => 'woocommerce_enable_review_rating', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + 'show_if_checked' => 'option', + ), + + array( + 'desc' => __( 'Star ratings should be required, not optional', 'woocommerce' ), + 'id' => 'woocommerce_review_rating_required', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => 'end', 'show_if_checked' => 'yes', - 'autoload' => false - ), - - array( 'type' => 'sectionend', 'id' => 'product_data_options' ), - - array( 'title' => __( 'Product Image Sizes', 'woocommerce' ), 'type' => 'title','desc' => sprintf(__( 'These settings affect the actual dimensions of images in your catalog - the display on the front-end will still be affected by CSS styles. After changing these settings you may need to regenerate your thumbnails.', 'woocommerce' ), 'http://wordpress.org/extend/plugins/regenerate-thumbnails/'), 'id' => 'image_options' ), - - array( - 'title' => __( 'Catalog Images', 'woocommerce' ), - 'desc' => __( 'This size is usually used in product listings', 'woocommerce' ), - 'id' => 'shop_catalog_image_size', - 'css' => '', - 'type' => 'image_width', - 'default' => array( - 'width' => '150', - 'height' => '150', - 'crop' => true - ), - 'desc_tip' => true, + 'autoload' => false, ), array( - 'title' => __( 'Single Product Image', 'woocommerce' ), - 'desc' => __( 'This is the size used by the main image on the product page.', 'woocommerce' ), - 'id' => 'shop_single_image_size', - 'css' => '', - 'type' => 'image_width', - 'default' => array( - 'width' => '300', - 'height' => '300', - 'crop' => 1 - ), - 'desc_tip' => true, + 'type' => 'sectionend', + 'id' => 'product_rating_options', ), - array( - 'title' => __( 'Product Thumbnails', 'woocommerce' ), - 'desc' => __( 'This size is usually used for the gallery of images on the product page.', 'woocommerce' ), - 'id' => 'shop_thumbnail_image_size', - 'css' => '', - 'type' => 'image_width', - 'default' => array( - 'width' => '90', - 'height' => '90', - 'crop' => 1 - ), - 'desc_tip' => true, - ), - - array( 'type' => 'sectionend', 'id' => 'image_options' ), - - array( 'title' => __( 'Downloadable Products', 'woocommerce' ), 'type' => 'title', 'id' => 'digital_download_options' ), - - array( - 'title' => __( 'File Download Method', 'woocommerce' ), - 'desc' => __( 'Forcing downloads will keep URLs hidden, but some servers may serve large files unreliably. If supported, X-Accel-Redirect/ X-Sendfile can be used to serve downloads instead (server requires mod_xsendfile).', 'woocommerce' ), - 'id' => 'woocommerce_file_download_method', - 'type' => 'select', - 'class' => 'chosen_select', - 'css' => 'min-width:300px;', - 'default' => 'force', - 'desc_tip' => true, - 'options' => array( - 'force' => __( 'Force Downloads', 'woocommerce' ), - 'xsendfile' => __( 'X-Accel-Redirect/X-Sendfile', 'woocommerce' ), - 'redirect' => __( 'Redirect only', 'woocommerce' ), - ), - 'autoload' => false - ), - - array( - 'title' => __( 'Access Restriction', 'woocommerce' ), - 'desc' => __( 'Downloads require login', 'woocommerce' ), - 'id' => 'woocommerce_downloads_require_login', - 'type' => 'checkbox', - 'default' => 'no', - 'desc_tip' => __( 'This setting does not apply to guest purchases.', 'woocommerce' ), - 'checkboxgroup' => 'start', - 'autoload' => false - ), - - array( - 'desc' => __( 'Grant access to downloadable products after payment', 'woocommerce' ), - 'id' => 'woocommerce_downloads_grant_access_after_payment', - 'type' => 'checkbox', - 'default' => 'yes', - 'desc_tip' => __( 'Enable this option to grant access to downloads when orders are "processing", rather than "completed".', 'woocommerce' ), - 'checkboxgroup' => 'end', - 'autoload' => false - ), - - array( 'type' => 'sectionend', 'id' => 'digital_download_options' ), - )); } + + return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $current_section ); } } endif; -return new WC_Settings_Products(); \ No newline at end of file +return new WC_Settings_Products(); diff --git a/includes/admin/settings/class-wc-settings-shipping.php b/includes/admin/settings/class-wc-settings-shipping.php index a4430aafc59..f510bf83411 100644 --- a/includes/admin/settings/class-wc-settings-shipping.php +++ b/includes/admin/settings/class-wc-settings-shipping.php @@ -2,18 +2,20 @@ /** * WooCommerce Shipping Settings * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin - * @version 2.1.0 + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin + * @version 2.6.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; +} -if ( ! class_exists( 'WC_Settings_Shipping' ) ) : +if ( ! class_exists( 'WC_Settings_Shipping', false ) ) : /** - * WC_Settings_Shipping + * WC_Settings_Shipping. */ class WC_Settings_Shipping extends WC_Settings_Page { @@ -23,251 +25,330 @@ class WC_Settings_Shipping extends WC_Settings_Page { public function __construct() { $this->id = 'shipping'; $this->label = __( 'Shipping', 'woocommerce' ); - add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 ); add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) ); add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) ); - add_action( 'woocommerce_admin_field_shipping_methods', array( $this, 'shipping_methods_setting' ) ); add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) ); } /** - * Get sections + * Add this page to settings. + * + * @param array $pages + * + * @return array|mixed + */ + public function add_settings_page( $pages ) { + return wc_shipping_enabled() ? parent::add_settings_page( $pages ) : $pages; + } + + /** + * Get sections. * * @return array */ public function get_sections() { $sections = array( - '' => __( 'Shipping Options', 'woocommerce' ) + '' => __( 'Shipping zones', 'woocommerce' ), + 'options' => __( 'Shipping options', 'woocommerce' ), + 'classes' => __( 'Shipping classes', 'woocommerce' ), ); - // Load shipping methods so we can show any global options they may have - $shipping_methods = WC()->shipping->load_shipping_methods(); + if ( ! defined( 'WC_INSTALLING' ) ) { + // Load shipping methods so we can show any global options they may have + $shipping_methods = WC()->shipping->load_shipping_methods(); - foreach ( $shipping_methods as $method ) { - - if ( ! $method->has_settings() ) continue; - - $title = empty( $method->method_title ) ? ucfirst( $method->id ) : $method->method_title; - - $sections[ strtolower( get_class( $method ) ) ] = esc_html( $title ); + foreach ( $shipping_methods as $method ) { + if ( ! $method->has_settings() ) { + continue; + } + $title = empty( $method->method_title ) ? ucfirst( $method->id ) : $method->method_title; + $sections[ strtolower( $method->id ) ] = esc_html( $title ); + } } return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); } /** - * Get settings array + * Get settings array. + * + * @param string $current_section * * @return array */ - public function get_settings() { - return apply_filters('woocommerce_shipping_settings', array( + public function get_settings( $current_section = '' ) { + $settings = array(); - array( 'title' => __( 'Shipping Options', 'woocommerce' ), 'type' => 'title', 'id' => 'shipping_options' ), + if ( '' === $current_section ) { + $settings = apply_filters( 'woocommerce_shipping_settings', array( - array( - 'title' => __( 'Shipping Calculations', 'woocommerce' ), - 'desc' => __( 'Enable shipping', 'woocommerce' ), - 'id' => 'woocommerce_calc_shipping', - 'default' => 'yes', - 'type' => 'checkbox', - 'checkboxgroup' => 'start' - ), + array( 'title' => __( 'Shipping options', 'woocommerce' ), 'type' => 'title', 'id' => 'shipping_options' ), - array( - 'desc' => __( 'Enable the shipping calculator on the cart page', 'woocommerce' ), - 'id' => 'woocommerce_enable_shipping_calc', - 'default' => 'yes', - 'type' => 'checkbox', - 'checkboxgroup' => '', - 'autoload' => false - ), - - array( - 'desc' => __( 'Hide shipping costs until an address is entered', 'woocommerce' ), - 'id' => 'woocommerce_shipping_cost_requires_address', - 'default' => 'no', - 'type' => 'checkbox', - 'checkboxgroup' => 'end', - 'autoload' => false - ), - - array( - 'title' => __( 'Shipping Display Mode', 'woocommerce' ), - 'desc' => __( 'This controls how multiple shipping methods are displayed on the frontend.', 'woocommerce' ), - 'id' => 'woocommerce_shipping_method_format', - 'default' => '', - 'type' => 'radio', - 'options' => array( - '' => __( 'Display shipping methods with "radio" buttons', 'woocommerce' ), - 'select' => __( 'Display shipping methods in a dropdown', 'woocommerce' ), + array( + 'title' => __( 'Calculations', 'woocommerce' ), + 'desc' => __( 'Enable the shipping calculator on the cart page', 'woocommerce' ), + 'id' => 'woocommerce_enable_shipping_calc', + 'default' => 'yes', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + 'autoload' => false, ), - 'desc_tip' => true, - 'autoload' => false - ), - array( - 'title' => __( 'Shipping Destination', 'woocommerce' ), - 'desc' => __( 'This controls which shipping address is used by default.', 'woocommerce' ), - 'id' => 'woocommerce_ship_to_destination', - 'default' => 'shipping', - 'type' => 'radio', - 'options' => array( - 'shipping' => __( 'Default to shipping address', 'woocommerce' ), - 'billing' => __( 'Default to billing address', 'woocommerce' ), - 'billing_only' => __( 'Only ship to the users billing address', 'woocommerce' ), + array( + 'desc' => __( 'Hide shipping costs until an address is entered', 'woocommerce' ), + 'id' => 'woocommerce_shipping_cost_requires_address', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'end', + 'autoload' => false, ), - 'autoload' => false, - 'desc_tip' => true, - 'show_if_checked' => 'option', - ), - array( - 'title' => __( 'Restrict shipping to Location(s)', 'woocommerce' ), - 'desc' => sprintf( __( 'Choose which countries you want to ship to, or choose to ship to all locations you sell to.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=general' ) ), - 'id' => 'woocommerce_ship_to_countries', - 'default' => '', - 'type' => 'select', - 'class' => 'chosen_select', - 'desc_tip' => false, - 'options' => array( - '' => __( 'Ship to all countries you sell to', 'woocommerce' ), - 'all' => __( 'Ship to all countries', 'woocommerce' ), - 'specific' => __( 'Ship to specific countries only', 'woocommerce' ) - ) - ), + array( + 'title' => __( 'Shipping destination', 'woocommerce' ), + 'desc' => __( 'This controls which shipping address is used by default.', 'woocommerce' ), + 'id' => 'woocommerce_ship_to_destination', + 'default' => 'billing', + 'type' => 'radio', + 'options' => array( + 'shipping' => __( 'Default to customer shipping address', 'woocommerce' ), + 'billing' => __( 'Default to customer billing address', 'woocommerce' ), + 'billing_only' => __( 'Force shipping to the customer billing address', 'woocommerce' ), + ), + 'autoload' => false, + 'desc_tip' => true, + 'show_if_checked' => 'option', + ), - array( - 'title' => __( 'Specific Countries', 'woocommerce' ), - 'desc' => '', - 'id' => 'woocommerce_specific_ship_to_countries', - 'css' => '', - 'default' => '', - 'type' => 'multi_select_countries' - ), + array( + 'title' => __( 'Debug mode', 'woocommerce' ), + 'desc' => __( 'Enable debug mode', 'woocommerce' ), + 'desc_tip' => __( 'Enable shipping debug mode to show matching shipping zones and to bypass shipping rate cache.', 'woocommerce' ), + 'id' => 'woocommerce_shipping_debug_mode', + 'default' => 'no', + 'type' => 'checkbox', + 'autoload' => false, + ), - array( - 'type' => 'shipping_methods', - ), + array( 'type' => 'sectionend', 'id' => 'shipping_options' ), - array( 'type' => 'sectionend', 'id' => 'shipping_options' ), + ) ); + } - )); // End shipping settings + return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $current_section ); } /** - * Output the settings + * Output the settings. */ public function output() { - global $current_section; + global $current_section, $hide_save_button; // Load shipping methods so we can show any global options they may have $shipping_methods = WC()->shipping->load_shipping_methods(); - if ( $current_section ) { - foreach ( $shipping_methods as $method ) { - if ( strtolower( get_class( $method ) ) == strtolower( $current_section ) && $method->has_settings() ) { + if ( '' === $current_section ) { + $this->output_zones_screen(); + } elseif ( 'options' === $current_section ) { + $settings = $this->get_settings(); + WC_Admin_Settings::output_fields( $settings ); + } elseif ( 'classes' === $current_section ) { + $hide_save_button = true; + $this->output_shipping_class_screen(); + } else { + foreach ( $shipping_methods as $method ) { + if ( in_array( $current_section, array( $method->id, sanitize_title( get_class( $method ) ) ) ) && $method->has_settings() ) { $method->admin_options(); - break; } } - } else { - $settings = $this->get_settings(); - - WC_Admin_Settings::output_fields( $settings ); } } /** - * Output shipping method settings. - * - * @access public - * @return void - */ - public function shipping_methods_setting() { - $default_shipping_method = esc_attr( get_option('woocommerce_default_shipping_method') ); - ?> - - - - - - - - - - - - - - - shipping->load_shipping_methods() as $key => $method ) { - echo ' - - - - - - '; - } - ?> - - - - - - - - -
     
    - id, false ) . ' /> - - - ' . $method->get_title() . ' - - ' . $method->id . ' - '; - - if ( $method->enabled == 'yes' ) - echo '' . __ ( 'Enabled', 'woocommerce' ) . ''; - else - echo '-'; - - echo ''; - - if ( $method->has_settings ) { - echo '' . __( 'Settings', 'woocommerce' ) . ''; - } - - echo '
    - /> - [?]
    - - - get_settings() ); + break; + case 'classes' : + case '' : + break; + default : + $wc_shipping = WC_Shipping::instance(); - $settings = $this->get_settings(); - - WC_Admin_Settings::save_fields( $settings ); - WC()->shipping->process_admin_options(); - - } elseif ( class_exists( $current_section ) ) { - - $current_section_class = new $current_section(); - - do_action( 'woocommerce_update_options_' . $this->id . '_' . $current_section_class->id ); + foreach ( $wc_shipping->get_shipping_methods() as $method_id => $method ) { + if ( in_array( $current_section, array( $method->id, sanitize_title( get_class( $method ) ) ) ) ) { + do_action( 'woocommerce_update_options_' . $this->id . '_' . $method->id ); + } + } + break; } + + // Increments the transient version to invalidate cache + WC_Cache_Helper::get_transient_version( 'shipping', true ); + } + + /** + * Handles output of the shipping zones page in admin. + */ + protected function output_zones_screen() { + global $hide_save_button; + + if ( isset( $_REQUEST['zone_id'] ) ) { + $hide_save_button = true; + $this->zone_methods_screen( wc_clean( $_REQUEST['zone_id'] ) ); + } elseif ( isset( $_REQUEST['instance_id'] ) ) { + $this->instance_settings_screen( absint( $_REQUEST['instance_id'] ) ); + } else { + $hide_save_button = true; + $this->zones_screen(); + } + } + + /** + * Show method for a zone + * @param int $zone_id + */ + protected function zone_methods_screen( $zone_id ) { + if ( 'new' === $zone_id ) { + $zone = new WC_Shipping_Zone(); + } else { + $zone = WC_Shipping_Zones::get_zone( absint( $zone_id ) ); + } + + if ( ! $zone ) { + wp_die( __( 'Zone does not exist!', 'woocommerce' ) ); + } + + $allowed_countries = WC()->countries->get_allowed_countries(); + $wc_shipping = WC_Shipping ::instance(); + $shipping_methods = $wc_shipping->get_shipping_methods(); + $continents = WC()->countries->get_continents(); + + // Prepare locations + $locations = array(); + $postcodes = array(); + + foreach ( $zone->get_zone_locations() as $location ) { + if ( 'postcode' === $location->type ) { + $postcodes[] = $location->code; + } else { + $locations[] = $location->type . ':' . $location->code; + } + } + + wp_localize_script( 'wc-shipping-zone-methods', 'shippingZoneMethodsLocalizeScript', array( + 'methods' => $zone->get_shipping_methods(), + 'zone_name' => $zone->get_zone_name(), + 'zone_id' => $zone->get_id(), + 'wc_shipping_zones_nonce' => wp_create_nonce( 'wc_shipping_zones_nonce' ), + 'strings' => array( + 'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ), + 'save_changes_prompt' => __( 'Do you wish to save your changes first? Your changed data will be discarded if you choose to cancel.', 'woocommerce' ), + 'save_failed' => __( 'Your changes were not saved. Please retry.', 'woocommerce' ), + 'add_method_failed' => __( 'Shipping method could not be added. Please retry.', 'woocommerce' ), + 'yes' => __( 'Yes', 'woocommerce' ), + 'no' => __( 'No', 'woocommerce' ), + 'default_zone_name' => __( 'Zone', 'woocommerce' ), + ), + ) ); + wp_enqueue_script( 'wc-shipping-zone-methods' ); + + include_once( dirname( __FILE__ ) . '/views/html-admin-page-shipping-zone-methods.php' ); + } + + /** + * Show zones + */ + protected function zones_screen() { + $allowed_countries = WC()->countries->get_allowed_countries(); + $continents = WC()->countries->get_continents(); + $method_count = wc_get_shipping_method_count(); + + wp_localize_script( 'wc-shipping-zones', 'shippingZonesLocalizeScript', array( + 'zones' => WC_Shipping_Zones::get_zones(), + 'default_zone' => array( + 'zone_id' => 0, + 'zone_name' => '', + 'zone_order' => null, + ), + 'wc_shipping_zones_nonce' => wp_create_nonce( 'wc_shipping_zones_nonce' ), + 'strings' => array( + 'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ), + 'delete_confirmation_msg' => __( 'Are you sure you want to delete this zone? This action cannot be undone.', 'woocommerce' ), + 'save_failed' => __( 'Your changes were not saved. Please retry.', 'woocommerce' ), + 'no_shipping_methods_offered' => __( 'No shipping methods offered to this zone.', 'woocommerce' ), + ), + ) ); + wp_enqueue_script( 'wc-shipping-zones' ); + + include_once( dirname( __FILE__ ) . '/views/html-admin-page-shipping-zones.php' ); + } + + /** + * Show instance settings + * @param int $instance_id + */ + protected function instance_settings_screen( $instance_id ) { + $zone = WC_Shipping_Zones::get_zone_by( 'instance_id', $instance_id ); + $shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id ); + + if ( ! $shipping_method ) { + wp_die( __( 'Invalid shipping method!', 'woocommerce' ) ); + } + if ( ! $zone ) { + wp_die( __( 'Zone does not exist!', 'woocommerce' ) ); + } + if ( ! $shipping_method->has_settings() ) { + wp_die( __( 'This shipping method does not have any settings to configure.', 'woocommerce' ) ); + } + + if ( ! empty( $_POST['save'] ) ) { + + if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-settings' ) ) { + echo '

    ' . __( 'Edit failed. Please try again.', 'woocommerce' ) . '

    '; + } + + $shipping_method->process_admin_options(); + $shipping_method->display_errors(); + } + + include_once( dirname( __FILE__ ) . '/views/html-admin-page-shipping-zones-instance.php' ); + } + + /** + * Handles output of the shipping class settings screen. + */ + protected function output_shipping_class_screen() { + $wc_shipping = WC_Shipping::instance(); + wp_localize_script( 'wc-shipping-classes', 'shippingClassesLocalizeScript', array( + 'classes' => $wc_shipping->get_shipping_classes(), + 'default_shipping_class' => array( + 'term_id' => 0, + 'name' => '', + 'description' => '', + ), + 'wc_shipping_classes_nonce' => wp_create_nonce( 'wc_shipping_classes_nonce' ), + 'strings' => array( + 'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ), + 'save_failed' => __( 'Your changes were not saved. Please retry.', 'woocommerce' ), + ), + ) ); + wp_enqueue_script( 'wc-shipping-classes' ); + + // Extendable columns to show on the shipping classes screen. + $shipping_class_columns = apply_filters( 'woocommerce_shipping_classes_columns', array( + 'wc-shipping-class-name' => __( 'Shipping class', 'woocommerce' ), + 'wc-shipping-class-slug' => __( 'Slug', 'woocommerce' ), + 'wc-shipping-class-description' => __( 'Description', 'woocommerce' ), + 'wc-shipping-class-count' => __( 'Product count', 'woocommerce' ), + ) ); + + include_once( dirname( __FILE__ ) . '/views/html-admin-page-shipping-classes.php' ); } } diff --git a/includes/admin/settings/class-wc-settings-tax.php b/includes/admin/settings/class-wc-settings-tax.php index 405aff5de8b..958390bd60b 100644 --- a/includes/admin/settings/class-wc-settings-tax.php +++ b/includes/admin/settings/class-wc-settings-tax.php @@ -2,203 +2,100 @@ /** * WooCommerce Tax Settings * - * @author WooThemes - * @category Admin - * @package WooCommerce/Admin + * @author WooThemes + * @category Admin + * @package WooCommerce/Admin * @version 2.1.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; +} -if ( ! class_exists( 'WC_Settings_Tax' ) ) : +if ( ! class_exists( 'WC_Settings_Tax', false ) ) : /** - * WC_Settings_Tax + * WC_Settings_Tax. */ class WC_Settings_Tax extends WC_Settings_Page { + /** + * Setting page id. + * + * @var string + */ + protected $id = 'tax'; + /** * Constructor. */ public function __construct() { - $this->id = 'tax'; $this->label = __( 'Tax', 'woocommerce' ); - - add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 ); - add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) ); - add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) ); - add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) ); + parent::__construct(); } /** - * Get sections + * Add this page to settings. + * + * @param array $pages + * + * @return array|mixed + */ + public function add_settings_page( $pages ) { + if ( wc_tax_enabled() ) { + return parent::add_settings_page( $pages ); + } else { + return $pages; + } + } + + /** + * Get sections. * * @return array */ public function get_sections() { $sections = array( - '' => __( 'Tax Options', 'woocommerce' ), - 'standard' => __( 'Standard Rates', 'woocommerce' ) + '' => __( 'Tax options', 'woocommerce' ), + 'standard' => __( 'Standard rates', 'woocommerce' ), ); // Get tax classes and display as links - $tax_classes = array_filter( array_map( 'trim', explode( "\n", get_option('woocommerce_tax_classes' ) ) ) ); + $tax_classes = WC_Tax::get_tax_classes(); - if ( $tax_classes ) - foreach ( $tax_classes as $class ) - $sections[ sanitize_title( $class ) ] = sprintf( __( '%s Rates', 'woocommerce' ), $class ); + foreach ( $tax_classes as $class ) { + $sections[ sanitize_title( $class ) ] = sprintf( __( '%s rates', 'woocommerce' ), $class ); + } return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections ); } /** - * Get settings array + * Get settings array. * + * @param string $current_section * @return array */ - public function get_settings() { - $tax_classes = array_filter( array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ) ); - $classes_options = array(); - if ( $tax_classes ) - foreach ( $tax_classes as $class ) - $classes_options[ sanitize_title( $class ) ] = esc_html( $class ); + public function get_settings( $current_section = '' ) { + $settings = array(); - return apply_filters('woocommerce_tax_settings', array( - - array( 'title' => __( 'Tax Options', 'woocommerce' ), 'type' => 'title','desc' => '', 'id' => 'tax_options' ), - - array( - 'title' => __( 'Enable Taxes', 'woocommerce' ), - 'desc' => __( 'Enable taxes and tax calculations', 'woocommerce' ), - 'id' => 'woocommerce_calc_taxes', - 'default' => 'no', - 'type' => 'checkbox' - ), - - array( - 'title' => __( 'Prices Entered With Tax', 'woocommerce' ), - 'id' => 'woocommerce_prices_include_tax', - 'default' => 'no', - 'type' => 'radio', - 'desc_tip' => __( 'This option is important as it will affect how you input prices. Changing it will not update existing products.', 'woocommerce' ), - 'options' => array( - 'yes' => __( 'Yes, I will enter prices inclusive of tax', 'woocommerce' ), - 'no' => __( 'No, I will enter prices exclusive of tax', 'woocommerce' ) - ), - ), - - array( - 'title' => __( 'Calculate Tax Based On:', 'woocommerce' ), - 'id' => 'woocommerce_tax_based_on', - 'desc_tip' => __( 'This option determines which address is used to calculate tax.', 'woocommerce' ), - 'default' => 'shipping', - 'type' => 'select', - 'options' => array( - 'shipping' => __( 'Customer shipping address', 'woocommerce' ), - 'billing' => __( 'Customer billing address', 'woocommerce' ), - 'base' => __( 'Shop base address', 'woocommerce' ) - ), - ), - - array( - 'title' => __( 'Default Customer Address:', 'woocommerce' ), - 'id' => 'woocommerce_default_customer_address', - 'desc_tip' => __( 'This option determines the customers default address (before they input their own).', 'woocommerce' ), - 'default' => 'base', - 'type' => 'select', - 'options' => array( - '' => __( 'No address', 'woocommerce' ), - 'base' => __( 'Shop base address', 'woocommerce' ), - ), - ), - - array( - 'title' => __( 'Shipping Tax Class:', 'woocommerce' ), - 'desc' => __( 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', 'woocommerce' ), - 'id' => 'woocommerce_shipping_tax_class', - 'css' => 'min-width:150px;', - 'default' => 'title', - 'type' => 'select', - 'options' => array( '' => __( 'Shipping tax class based on cart items', 'woocommerce' ), 'standard' => __( 'Standard', 'woocommerce' ) ) + $classes_options, - 'desc_tip' => true, - ), - - array( - 'title' => __( 'Rounding', 'woocommerce' ), - 'desc' => __( 'Round tax at subtotal level, instead of rounding per line', 'woocommerce' ), - 'id' => 'woocommerce_tax_round_at_subtotal', - 'default' => 'no', - 'type' => 'checkbox', - ), - - array( - 'title' => __( 'Additional Tax Classes', 'woocommerce' ), - 'desc' => __( 'List additional tax classes below (1 per line). This is in addition to the default Standard Rate. Tax classes can be assigned to products.', 'woocommerce' ), - 'id' => 'woocommerce_tax_classes', - 'css' => 'width:100%; height: 65px;', - 'type' => 'textarea', - 'default' => sprintf( __( 'Reduced Rate%sZero Rate', 'woocommerce' ), PHP_EOL ) - ), - - array( - 'title' => __( 'Display prices in the shop:', 'woocommerce' ), - 'id' => 'woocommerce_tax_display_shop', - 'default' => 'excl', - 'type' => 'select', - 'options' => array( - 'incl' => __( 'Including tax', 'woocommerce' ), - 'excl' => __( 'Excluding tax', 'woocommerce' ), - ) - ), - - array( - 'title' => __( 'Price display suffix:', 'woocommerce' ), - 'id' => 'woocommerce_price_display_suffix', - 'default' => '', - 'type' => 'text', - 'desc' => __( 'Define text to show after your product prices. This could be, for example, "inc. Vat" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.', 'woocommerce' ), - ), - - array( - 'title' => __( 'Display prices during cart/checkout:', 'woocommerce' ), - 'id' => 'woocommerce_tax_display_cart', - 'default' => 'excl', - 'type' => 'select', - 'options' => array( - 'incl' => __( 'Including tax', 'woocommerce' ), - 'excl' => __( 'Excluding tax', 'woocommerce' ), - ), - 'autoload' => false - ), - - array( - 'title' => __( 'Display tax totals:', 'woocommerce' ), - 'id' => 'woocommerce_tax_total_display', - 'default' => 'itemized', - 'type' => 'select', - 'options' => array( - 'single' => __( 'As a single total', 'woocommerce' ), - 'itemized' => __( 'Itemized', 'woocommerce' ), - ), - 'autoload' => false - ), - - array( 'type' => 'sectionend', 'id' => 'tax_options' ), - - )); // End tax settings + if ( '' === $current_section ) { + $settings = include( 'views/settings-tax.php' ); + } + return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $current_section ); } /** - * Output the settings + * Output the settings. */ public function output() { global $current_section; - $tax_classes = array_filter( array_map( 'trim', explode( "\n", get_option('woocommerce_tax_classes' ) ) ) ); + $tax_classes = WC_Tax::get_tax_class_slugs(); - if ( $current_section == 'standard' || in_array( $current_section, array_map( 'sanitize_title', $tax_classes ) ) ) { - $this->output_tax_rates(); - } else { + if ( 'standard' === $current_section || in_array( $current_section, $tax_classes ) ) { + $this->output_tax_rates(); + } else { $settings = $this->get_settings(); WC_Admin_Settings::output_fields( $settings ); @@ -206,536 +103,188 @@ class WC_Settings_Tax extends WC_Settings_Page { } /** - * Save settings + * Save settings. */ public function save() { - global $current_section, $wpdb; + global $current_section; if ( ! $current_section ) { - $settings = $this->get_settings(); WC_Admin_Settings::save_fields( $settings ); - } else { - + } elseif ( ! empty( $_POST['tax_rate_country'] ) ) { $this->save_tax_rates(); - } - $wpdb->query( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE ('_transient_wc_tax_rates_%') OR `option_name` LIKE ('_transient_timeout_wc_tax_rates_%')" ); + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); } /** - * Output tax rate tables + * Output tax rate tables. */ public function output_tax_rates() { - global $current_section, $wpdb; + global $current_section; - $page = ! empty( $_GET['p'] ) ? absint( $_GET['p'] ) : 1; - $limit = 100; - $tax_classes = array_filter( array_map( 'trim', explode( "\n", get_option('woocommerce_tax_classes' ) ) ) ); - $current_class = ''; + $current_class = $this->get_current_tax_class(); - foreach( $tax_classes as $class ) - if ( sanitize_title( $class ) == $current_section ) - $current_class = $class; - ?> -

    -

    See here for available alpha-2 country codes.', 'woocommerce' ), 'http://en.wikipedia.org/wiki/ISO_3166-1#Current_codes' ); ?>

    - - - - + $countries = array(); + foreach ( WC()->countries->get_allowed_countries() as $value => $label ) { + $countries[] = array( + 'value' => $value, + 'label' => esc_js( html_entity_decode( $label ) ), + ); + } - + $states = array(); + foreach ( WC()->countries->get_allowed_country_states() as $label ) { + foreach ( $label as $code => $state ) { + $states[] = array( + 'value' => $code, + 'label' => esc_js( html_entity_decode( $state ) ), + ); + } + } - + $base_url = admin_url( add_query_arg( array( + 'page' => 'wc-settings', + 'tab' => 'tax', + 'section' => $current_section, + ), 'admin.php' ) ); - + // Localize and enqueue our js. + wp_localize_script( 'wc-settings-tax', 'htmlSettingsTaxLocalizeScript', array( + 'current_class' => $current_class, + 'wc_tax_nonce' => wp_create_nonce( 'wc_tax_nonce-class:' . $current_class ), + 'base_url' => $base_url, + 'rates' => array_values( WC_Tax::get_rates_for_tax_class( $current_class ) ), + 'page' => ! empty( $_GET['p'] ) ? absint( $_GET['p'] ) : 1, + 'limit' => 100, + 'countries' => $countries, + 'states' => $states, + 'default_rate' => array( + 'tax_rate_id' => 0, + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '', + 'tax_rate_name' => '', + 'tax_rate_priority' => 1, + 'tax_rate_compound' => 0, + 'tax_rate_shipping' => 1, + 'tax_rate_order' => null, + 'tax_rate_class' => $current_class, + ), + 'strings' => array( + 'no_rows_selected' => __( 'No row(s) selected', 'woocommerce' ), + 'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ), + 'csv_data_cols' => array( + __( 'Country code', 'woocommerce' ), + __( 'State code', 'woocommerce' ), + __( 'Postcode / ZIP', 'woocommerce' ), + __( 'City', 'woocommerce' ), + __( 'Rate %', 'woocommerce' ), + __( 'Tax name', 'woocommerce' ), + __( 'Priority', 'woocommerce' ), + __( 'Compound', 'woocommerce' ), + __( 'Shipping', 'woocommerce' ), + __( 'Tax class', 'woocommerce' ), + ), + ), + ) ); + wp_enqueue_script( 'wc-settings-tax' ); - - - - - - - - - - - - - - - - get_results( $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates - WHERE tax_rate_class = %s - ORDER BY tax_rate_order - LIMIT %d, %d - " , - sanitize_title( $current_class ), - ( $page - 1 ) * $limit, - $limit - ) ); - - foreach ( $rates as $rate ) { - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      [?] [?] [?] [?] [?] [?] [?] [?] [?]
    - - - - - prefix}woocommerce_tax_rate_locations WHERE location_type='postcode' AND tax_rate_id = %d ORDER BY location_code", $rate->tax_rate_id ) ); - - echo esc_attr( implode( '; ', $locations ) ); - ?>" placeholder="*" data-name="tax_rate_postcode[tax_rate_id ?>]" /> - - prefix}woocommerce_tax_rate_locations WHERE location_type='city' AND tax_rate_id = %d ORDER BY location_code", $rate->tax_rate_id ) ); - echo esc_attr( implode( '; ', $locations ) ); - ?>" placeholder="*" data-name="tax_rate_city[tax_rate_id ?>]" /> - - - - - - - - tax_rate_compound, '1' ); ?> /> - - tax_rate_shipping, '1' ); ?> /> -
    - - - - - - - -
    - - get_current_tax_class() ); + + // get the tax rate id of the first submited row + $first_tax_rate_id = key( $_POST['tax_rate_country'] ); + + // get the order position of the first tax rate id + $tax_rate_order = absint( $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_order FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $first_tax_rate_id ) ) ); + + $index = isset( $tax_rate_order ) ? $tax_rate_order : 0; // Loop posted fields - foreach ( $tax_rate_country as $key => $value ) { - - // new keys are inserted... - if ( $key == 'new' ) { - - foreach ( $value as $new_key => $new_value ) { - - // Sanitize + format - $country = strtoupper( wc_clean( $tax_rate_country[ $key ][ $new_key ] ) ); - $state = strtoupper( wc_clean( $tax_rate_state[ $key ][ $new_key ] ) ); - $postcode = wc_clean( $tax_rate_postcode[ $key ][ $new_key ] ); - $city = wc_clean( $tax_rate_city[ $key ][ $new_key ] ); - $rate = number_format( wc_clean( $tax_rate[ $key ][ $new_key ] ), 4, '.', '' ); - $name = wc_clean( $tax_rate_name[ $key ][ $new_key ] ); - $priority = absint( wc_clean( $tax_rate_priority[ $key ][ $new_key ] ) ); - $compound = isset( $tax_rate_compound[ $key ][ $new_key ] ) ? 1 : 0; - $shipping = isset( $tax_rate_shipping[ $key ][ $new_key ] ) ? 1 : 0; - - if ( ! $name ) - $name = __( 'Tax', 'woocommerce' ); - - if ( $country == '*' ) - $country = ''; - - if ( $state == '*' ) - $state = ''; - - $_tax_rate = array( - 'tax_rate_country' => $country, - 'tax_rate_state' => $state, - 'tax_rate' => $rate, - 'tax_rate_name' => $name, - 'tax_rate_priority' => $priority, - 'tax_rate_compound' => $compound, - 'tax_rate_shipping' => $shipping, - 'tax_rate_order' => $i, - 'tax_rate_class' => sanitize_title( $current_class ) - ); - - $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', $_tax_rate ); - - $tax_rate_id = $wpdb->insert_id; - - do_action( 'woocommerce_tax_rate_added', $tax_rate_id, $_tax_rate ); - - if ( ! empty( $postcode ) ) { - $postcodes = explode( ';', $postcode ); - $postcodes = array_map( 'strtoupper', array_map( 'wc_clean', $postcodes ) ); - - $postcode_query = array(); - - foreach( $postcodes as $postcode ) - if ( strstr( $postcode, '-' ) ) { - $postcode_parts = explode( '-', $postcode ); - - if ( is_numeric( $postcode_parts[0] ) && is_numeric( $postcode_parts[1] ) && $postcode_parts[1] > $postcode_parts[0] ) { - for ( $i = $postcode_parts[0]; $i <= $postcode_parts[1]; $i ++ ) { - if ( ! $i ) - continue; - - if ( strlen( $i ) < strlen( $postcode_parts[0] ) ) - $i = str_pad( $i, strlen( $postcode_parts[0] ), "0", STR_PAD_LEFT ); - - $postcode_query[] = "( '" . esc_sql( $i ) . "', $tax_rate_id, 'postcode' )"; - } - } - } else { - if ( $postcode ) - $postcode_query[] = "( '" . esc_sql( $postcode ) . "', $tax_rate_id, 'postcode' )"; - } - - $wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES " . implode( ',', $postcode_query ) ); - } - - if ( ! empty( $city ) ) { - $cities = explode( ';', $city ); - $cities = array_map( 'strtoupper', array_map( 'wc_clean', $cities ) ); - foreach( $cities as $city ) { - $wpdb->insert( - $wpdb->prefix . "woocommerce_tax_rate_locations", - array( - 'location_code' => $city, - 'tax_rate_id' => $tax_rate_id, - 'location_type' => 'city', - ) - ); - } - } - - $i++; - } - - // ...whereas the others are updated - } else { + foreach ( $_POST['tax_rate_country'] as $key => $value ) { + $mode = ( 0 === strpos( $key, 'new-' ) ) ? 'insert' : 'update'; + $tax_rate = $this->get_posted_tax_rate( $key, $index ++, $current_class ); + if ( 'insert' === $mode ) { + $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate ); + } elseif ( 1 == $_POST['remove_tax_rate'][ $key ] ) { $tax_rate_id = absint( $key ); + WC_Tax::_delete_tax_rate( $tax_rate_id ); + continue; + } else { + $tax_rate_id = absint( $key ); + WC_Tax::_update_tax_rate( $tax_rate_id, $tax_rate ); + } - if ( $_POST['remove_tax_rate'][ $key ] == 1 ) { - do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id ); - - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) ); - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) ); - - continue; - } - - // Sanitize + format - $country = strtoupper( wc_clean( $tax_rate_country[ $key ] ) ); - $state = strtoupper( wc_clean( $tax_rate_state[ $key ] ) ); - $rate = number_format( (double) wc_clean( $tax_rate[ $key ] ), 4, '.', '' ); - $name = wc_clean( $tax_rate_name[ $key ] ); - $priority = absint( wc_clean( $tax_rate_priority[ $key ] ) ); - $compound = isset( $tax_rate_compound[ $key ] ) ? 1 : 0; - $shipping = isset( $tax_rate_shipping[ $key ] ) ? 1 : 0; - - if ( ! $name ) - $name = __( 'Tax', 'woocommerce' ); - - if ( $country == '*' ) - $country = ''; - - if ( $state == '*' ) - $state = ''; - - $_tax_rate = array( - 'tax_rate_country' => $country, - 'tax_rate_state' => $state, - 'tax_rate' => $rate, - 'tax_rate_name' => $name, - 'tax_rate_priority' => $priority, - 'tax_rate_compound' => $compound, - 'tax_rate_shipping' => $shipping, - 'tax_rate_order' => $i, - 'tax_rate_class' => sanitize_title( $current_class ) - ); - - $wpdb->update( - $wpdb->prefix . "woocommerce_tax_rates", - $_tax_rate, - array( - 'tax_rate_id' => $tax_rate_id - ) - ); - - do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $_tax_rate ); - - if ( isset( $tax_rate_postcode[ $key ] ) ) { - // Delete old - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = 'postcode';", $tax_rate_id ) ); - - // Add changed - $postcode = wc_clean( $tax_rate_postcode[ $key ] ); - $postcodes = explode( ';', $postcode ); - $postcodes = array_map( 'strtoupper', array_map( 'wc_clean', $postcodes ) ); - - $postcode_query = array(); - - foreach( $postcodes as $postcode ) - if ( strstr( $postcode, '-' ) ) { - $postcode_parts = explode( '-', $postcode ); - - if ( is_numeric( $postcode_parts[0] ) && is_numeric( $postcode_parts[1] ) && $postcode_parts[1] > $postcode_parts[0] ) { - for ( $i = $postcode_parts[0]; $i <= $postcode_parts[1]; $i ++ ) { - if ( ! $i ) - continue; - - if ( strlen( $i ) < strlen( $postcode_parts[0] ) ) - $i = str_pad( $i, strlen( $postcode_parts[0] ), "0", STR_PAD_LEFT ); - - $postcode_query[] = "( '" . esc_sql( $i ) . "', $tax_rate_id, 'postcode' )"; - } - } - } else { - if ( $postcode ) - $postcode_query[] = "( '" . esc_sql( $postcode ) . "', $tax_rate_id, 'postcode' )"; - } - - $wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES " . implode( ',', $postcode_query ) ); - - } - - if ( isset( $tax_rate_city[ $key ] ) ) { - // Delete old - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = 'city';", $tax_rate_id ) ); - - // Add changed - $city = wc_clean( $tax_rate_city[ $key ] ); - $cities = explode( ';', $city ); - $cities = array_map( 'strtoupper', array_map( 'wc_clean', $cities ) ); - foreach( $cities as $city ) { - if ( $city ) { - $wpdb->insert( - $wpdb->prefix . "woocommerce_tax_rate_locations", - array( - 'location_code' => $city, - 'tax_rate_id' => $tax_rate_id, - 'location_type' => 'city', - ) - ); - } - } - } - - $i++; + if ( isset( $_POST['tax_rate_postcode'][ $key ] ) ) { + WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, wc_clean( $_POST['tax_rate_postcode'][ $key ] ) ); + } + if ( isset( $_POST['tax_rate_city'][ $key ] ) ) { + WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( $_POST['tax_rate_city'][ $key ] ) ); } } } - } endif; diff --git a/includes/admin/settings/views/html-admin-page-shipping-classes.php b/includes/admin/settings/views/html-admin-page-shipping-classes.php new file mode 100644 index 00000000000..de8849e95f9 --- /dev/null +++ b/includes/admin/settings/views/html-admin-page-shipping-classes.php @@ -0,0 +1,84 @@ + + +

    + + +

    + + + + + $heading ) : ?> + + + + + + + + + + +
    + + +
    + + + + diff --git a/includes/admin/settings/views/html-admin-page-shipping-zone-methods.php b/includes/admin/settings/views/html-admin-page-shipping-zone-methods.php new file mode 100644 index 00000000000..b293677329c --- /dev/null +++ b/includes/admin/settings/views/html-admin-page-shipping-zone-methods.php @@ -0,0 +1,188 @@ + + +

    + > + get_zone_name() ? $zone->get_zone_name() : __( 'Zone', 'woocommerce' ) ); ?> +

    + + + + + + get_id() ) : ?> + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + + +
    + + 90210...99000) are also supported.', 'woocommerce' ) ?> +
    +
    + + + + + + + + + + + + + + + + + + +
    + +
    +
    +

    + +

    + + + + + + + + diff --git a/includes/admin/settings/views/html-admin-page-shipping-zones-instance.php b/includes/admin/settings/views/html-admin-page-shipping-zones-instance.php new file mode 100644 index 00000000000..82d273b53e4 --- /dev/null +++ b/includes/admin/settings/views/html-admin-page-shipping-zones-instance.php @@ -0,0 +1,12 @@ + +

    + > + get_zone_name() ); ?> > + get_method_title() ); ?> +

    + +admin_options(); ?> diff --git a/includes/admin/settings/views/html-admin-page-shipping-zones.php b/includes/admin/settings/views/html-admin-page-shipping-zones.php new file mode 100644 index 00000000000..04d2963167a --- /dev/null +++ b/includes/admin/settings/views/html-admin-page-shipping-zones.php @@ -0,0 +1,128 @@ + + +

    + + +

    +

    + + + + + + + + + + + + + + + + + + +
    + +
    + +
    +
    optionally used for regions that are not included in any other shipping zone.', 'woocommerce' ); ?> +
      + get_shipping_methods(); + uasort( $methods, 'wc_shipping_zone_method_order_uasort_comparison' ); + + if ( ! empty( $methods ) ) { + foreach ( $methods as $method ) { + $class_name = 'yes' === $method->enabled ? 'method_enabled' : 'method_disabled'; + echo '
    • ' . esc_html( $method->get_title() ) . '
    • '; + } + } else { + echo '
    • ' . __( 'No shipping methods offered to this zone.', 'woocommerce' ) . '
    • '; + } + ?> +
    +
    + + + + + + diff --git a/includes/admin/settings/views/html-keys-edit.php b/includes/admin/settings/views/html-keys-edit.php new file mode 100644 index 00000000000..7b7d339251f --- /dev/null +++ b/includes/admin/settings/views/html-keys-edit.php @@ -0,0 +1,143 @@ + + +
    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + display_name, + absint( $user->ID ), + $user->user_email + ); + ?> + +
    + + + + +
    + + + +
    + + + +
    + + + + +

    + + +

    + +
    + + diff --git a/includes/admin/settings/views/html-settings-tax.php b/includes/admin/settings/views/html-settings-tax.php new file mode 100644 index 00000000000..f9d426cc301 --- /dev/null +++ b/includes/admin/settings/views/html-settings-tax.php @@ -0,0 +1,143 @@ + + + + +
    + +

    + + + + + + + + + + + + + + + + + + + + + + + + + +
             
    + + + + +
    + + + + + + diff --git a/includes/admin/settings/views/html-webhook-log.php b/includes/admin/settings/views/html-webhook-log.php new file mode 100644 index 00000000000..3c3be152c43 --- /dev/null +++ b/includes/admin/settings/views/html-webhook-log.php @@ -0,0 +1,38 @@ + + + + comment_date_gmt ), true ); ?> + + +

    :

    +

    :

    +

    :

    +
      + $value ) : ?> +
    • :
    • + +
    +

    :

    +
    + + +

    :

    +

    :

    +
      + getAll() : $log['response_headers']; ?> + $value ) : ?> +
    • :
    • + +
    + +

    :

    +

    + + + diff --git a/includes/admin/settings/views/html-webhook-logs.php b/includes/admin/settings/views/html-webhook-logs.php new file mode 100644 index 00000000000..c11824efacd --- /dev/null +++ b/includes/admin/settings/views/html-webhook-logs.php @@ -0,0 +1,42 @@ +id ); +$total = $count_comments->approved; + +?> + + + + + + + + + + + + + + + + + + + + + + get_delivery_log( $log->comment_ID ); + + include( 'html-webhook-log.php' ); + } + ?> + +
    + + diff --git a/includes/admin/settings/views/html-webhooks-edit.php b/includes/admin/settings/views/html-webhooks-edit.php new file mode 100644 index 00000000000..09d2701690d --- /dev/null +++ b/includes/admin/settings/views/html-webhooks-edit.php @@ -0,0 +1,205 @@ + + + + +
    +

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

    + + + post_data->post_modified_gmt ) : ?> + post_data->post_date_gmt ) : ?> + + + + + + + + + + + + + + + + + + + +
    + + + post_data->post_modified_gmt ) ); ?> +
    + + + post_data->post_date_gmt ) ); ?> +
    + + + post_data->post_modified_gmt ) ); ?> +
    +

    + + id ) ) : ?> + + +

    +
    +
    + +
    +

    + + +
    + + diff --git a/includes/admin/settings/views/settings-tax.php b/includes/admin/settings/views/settings-tax.php new file mode 100644 index 00000000000..846a91cde69 --- /dev/null +++ b/includes/admin/settings/views/settings-tax.php @@ -0,0 +1,121 @@ + __( 'Tax options', 'woocommerce' ), 'type' => 'title','desc' => '', 'id' => 'tax_options' ), + + array( + 'title' => __( 'Prices entered with tax', 'woocommerce' ), + 'id' => 'woocommerce_prices_include_tax', + 'default' => 'no', + 'type' => 'radio', + 'desc_tip' => __( 'This option is important as it will affect how you input prices. Changing it will not update existing products.', 'woocommerce' ), + 'options' => array( + 'yes' => __( 'Yes, I will enter prices inclusive of tax', 'woocommerce' ), + 'no' => __( 'No, I will enter prices exclusive of tax', 'woocommerce' ), + ), + ), + + array( + 'title' => __( 'Calculate tax based on', 'woocommerce' ), + 'id' => 'woocommerce_tax_based_on', + 'desc_tip' => __( 'This option determines which address is used to calculate tax.', 'woocommerce' ), + 'default' => 'shipping', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'options' => array( + 'shipping' => __( 'Customer shipping address', 'woocommerce' ), + 'billing' => __( 'Customer billing address', 'woocommerce' ), + 'base' => __( 'Shop base address', 'woocommerce' ), + ), + ), + + 'shipping-tax-class' => array( + 'title' => __( 'Shipping tax class', 'woocommerce' ), + 'desc' => __( 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', 'woocommerce' ), + 'id' => 'woocommerce_shipping_tax_class', + 'css' => 'min-width:150px;', + 'default' => 'inherit', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'options' => array_merge( array( 'inherit' => __( 'Shipping tax class based on cart items', 'woocommerce' ) ), wc_get_product_tax_class_options() ), + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Rounding', 'woocommerce' ), + 'desc' => __( 'Round tax at subtotal level, instead of rounding per line', 'woocommerce' ), + 'id' => 'woocommerce_tax_round_at_subtotal', + 'default' => 'no', + 'type' => 'checkbox', + ), + + array( + 'title' => __( 'Additional tax classes', 'woocommerce' ), + 'desc_tip' => __( 'List additional tax classes below (1 per line). This is in addition to the default "Standard rate".', 'woocommerce' ), + 'id' => 'woocommerce_tax_classes', + 'css' => 'width:100%; height: 65px;', + 'type' => 'textarea', + 'default' => sprintf( __( 'Reduced rate%sZero rate', 'woocommerce' ), PHP_EOL ), + ), + + array( + 'title' => __( 'Display prices in the shop', 'woocommerce' ), + 'id' => 'woocommerce_tax_display_shop', + 'default' => 'excl', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'options' => array( + 'incl' => __( 'Including tax', 'woocommerce' ), + 'excl' => __( 'Excluding tax', 'woocommerce' ), + ), + ), + + array( + 'title' => __( 'Display prices during cart and checkout', 'woocommerce' ), + 'id' => 'woocommerce_tax_display_cart', + 'default' => 'excl', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'options' => array( + 'incl' => __( 'Including tax', 'woocommerce' ), + 'excl' => __( 'Excluding tax', 'woocommerce' ), + ), + 'autoload' => false, + ), + + array( + 'title' => __( 'Price display suffix', 'woocommerce' ), + 'id' => 'woocommerce_price_display_suffix', + 'default' => '', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => __( 'Define text to show after your product prices. This could be, for example, "inc. Vat" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.', 'woocommerce' ), + ), + + array( + 'title' => __( 'Display tax totals', 'woocommerce' ), + 'id' => 'woocommerce_tax_total_display', + 'default' => 'itemized', + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'options' => array( + 'single' => __( 'As a single total', 'woocommerce' ), + 'itemized' => __( 'Itemized', 'woocommerce' ), + ), + 'autoload' => false, + ), + + array( 'type' => 'sectionend', 'id' => 'tax_options' ), + +); + +if ( ! wc_shipping_enabled() ) { + unset( $settings['shipping-tax-class'] ); +} + +return apply_filters( 'woocommerce_tax_settings', $settings ); diff --git a/includes/admin/views/html-admin-page-addons.php b/includes/admin/views/html-admin-page-addons.php index 919f5a544c1..07fa6587dac 100644 --- a/includes/admin/views/html-admin-page-addons.php +++ b/includes/admin/views/html-admin-page-addons.php @@ -1,84 +1,92 @@ +
    -

    -

    - - - -

    - + + +

    + +
      - __( 'Popular', 'woocommerce' ), - 'payment-gateways' => __( 'Gateways', 'woocommerce' ), - 'shipping-methods' => __( 'Shipping', 'woocommerce' ), - 'import-export-extensions' => __( 'Import/export', 'woocommerce' ), - 'product-extensions' => __( 'Products', 'woocommerce' ), - 'marketing-extensions' => __( 'Marketing', 'woocommerce' ), - 'accounting-extensions' => __( 'Accounting', 'woocommerce' ), - 'free-extensions' => __( 'Free', 'woocommerce' ), - 'third-party-extensions' => __( 'Third-party', 'woocommerce' ), - ); - - $i = 0; - - foreach ( $links as $link => $name ) { - $i ++; - ?>
    • + $section ) : ?> +
    • title ); ?>
    • +

    - + +

    WooCommerce Extensions Catalog', 'woocommerce' ), 'https://woocommerce.com/product-category/woocommerce-extensions/' ); ?>

    + -

    WooCommerce Extensions Catalog', 'woocommerce' ), 'http://www.woothemes.com/product-category/woocommerce-extensions/' ); ?>

    - + +
    + Storefront +

    +

    official WooCommerce theme.', 'woocommerce' ); ?>

    +

    free WordPress theme offering deep integration with WooCommerce and many of the most popular customer-facing extensions.', 'woocommerce' ); ?>

    +

    + + +

    +
    diff --git a/includes/admin/views/html-admin-page-product-export.php b/includes/admin/views/html-admin-page-product-export.php new file mode 100644 index 00000000000..34b3e227be4 --- /dev/null +++ b/includes/admin/views/html-admin-page-product-export.php @@ -0,0 +1,78 @@ +publish + $product_count->private + $variation_count->publish + $variation_count->private; +?> +
    +

    + +
    +
    +
    + +

    +

    +
    +
    + + + + + + + + + + + + + + + +
    + + + +
    + + + +
    + + + + +
    + +
    +
    + +
    +
    +
    +
    diff --git a/includes/admin/views/html-admin-page-reports.php b/includes/admin/views/html-admin-page-reports.php index da2259054cd..b8f5742eb51 100644 --- a/includes/admin/views/html-admin-page-reports.php +++ b/includes/admin/views/html-admin-page-reports.php @@ -1,15 +1,27 @@ +
    -

    + do_action( 'wc_reports_tabs' ); + ?> + 1 ) { ?>
      @@ -21,7 +33,9 @@ $link = '' . $report['title'] . ''; @@ -29,7 +43,7 @@ } - echo implode(' |
    • ', $links); + echo implode( ' |
    • ', $links ); ?>
    @@ -37,18 +51,23 @@ ' . $report['title'] . ''; + if ( ! isset( $report['hide_title'] ) || true != $report['hide_title'] ) { + echo '

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

    '; + } else { + echo '

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

    '; + } - if ( $report['description'] ) + if ( $report['description'] ) { echo '

    ' . $report['description'] . '

    '; + } - if ( $report['callback'] && ( is_callable( $report['callback'] ) ) ) + if ( $report['callback'] && ( is_callable( $report['callback'] ) ) ) { call_user_func( $report['callback'], $current_report ); + } } ?> -
    \ No newline at end of file +
    diff --git a/includes/admin/views/html-admin-page-status-logs-db.php b/includes/admin/views/html-admin-page-status-logs-db.php new file mode 100644 index 00000000000..c4b64377bda --- /dev/null +++ b/includes/admin/views/html-admin-page-status-logs-db.php @@ -0,0 +1,28 @@ + +
    + + display(); ?> + + + + + + +
    +
    -

    +

    + + + + +

    - +
    - +
    -

    - \ No newline at end of file +

    + diff --git a/includes/admin/views/html-admin-page-status-report.php b/includes/admin/views/html-admin-page-status-report.php index 07fbf626b61..b1e06a17003 100644 --- a/includes/admin/views/html-admin-page-status-report.php +++ b/includes/admin/views/html-admin-page-status-report.php @@ -1,585 +1,690 @@ -
    -

    -

    -
    -
    -
    - +get_environment_info(); +$database = $system_status->get_database_info(); +$active_plugins = $system_status->get_active_plugins(); +$theme = $system_status->get_theme_info(); +$security = $system_status->get_security_info(); +$settings = $system_status->get_settings(); +$pages = $system_status->get_pages(); +?> +
    +

    +

    +

    +
    + +

    + +
    +
    +
    - + - - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - - - - + + - + + + + + + + + + + +

    ::
    ::
    :version ); ?>:
    :: ' . esc_html( $environment['log_directory'] ) . ' '; + } else { + echo ' ' . sprintf( __( 'To allow logging, make %1$s writable or define a custom %2$s.', 'woocommerce' ), '' . $environment['log_directory'] . '', 'WC_LOG_DIR' ) . ''; + } + ?>
    ::
    ::' : '–'; ?>
    :: ' . sprintf( __( '%1$s - We recommend setting memory to at least 64MB. See: %2$s', 'woocommerce' ), size_format( $environment['wp_memory_limit'] ), '' . __( 'Increasing memory allocated to PHP', 'woocommerce' ) . '' ) . ''; + } else { + echo '' . size_format( $environment['wp_memory_limit'] ) . ''; + } + ?>
    :
    :: - db_version(); - ?> + + + + +
    :: + + + + + +
    :
    + + + + + + + + + + + + + + + - - - - - - - - - - - - + ?> - - + + + - - + + + - - + + + - - + + + + + + + + + + use_mysqli ) { + $ver = mysqli_get_server_info( $wpdb->dbh ); + } else { + $ver = mysql_get_server_info(); + } + if ( ! empty( $wpdb->is_mysql ) && ! stristr( $ver, 'MariaDB' ) ) : ?> + + + + - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + '; + } else { + $css_class = 'error'; + $icon = ''; + } + ?> + + + + + + +

    :
    : ' . sprintf( __( '%s - We recommend setting memory to at least 64MB. See: Increasing memory allocated to PHP', 'woocommerce' ), size_format( $memory ), 'http://codex.wordpress.org/Editing_wp-config.php#Increasing_memory_allocated_to_PHP' ) . ''; + if ( version_compare( $environment['php_version'], '5.6', '<' ) ) { + echo ' ' . sprintf( __( '%1$s - We recommend a minimum PHP version of 5.6. See: %2$s', 'woocommerce' ), esc_html( $environment['php_version'] ), '' . __( 'How to update your PHP version', 'woocommerce' ) . '' ) . ''; } else { - echo '' . size_format( $memory ) . ''; + echo '' . esc_html( $environment['php_version'] ) . ''; } - ?>
    :' . __( 'Yes', 'woocommerce' ) . ''; else echo '' . __( 'No', 'woocommerce' ) . ''; ?>
    :
    :
    ::
    ::
    ::
    ::
    :' : '–'; ?>
    : + ' . sprintf( __( '%1$s - We recommend a minimum MySQL version of 5.6. See: %2$s', 'woocommerce' ), esc_html( $environment['mysql_version'] ), '' . __( 'WordPress requirements', 'woocommerce' ) . '' ) . ''; + } else { + echo '' . esc_html( $environment['mysql_version'] ) . ''; + } + ?> +
    :' . __( 'Log directory (%s) is writable.', 'woocommerce' ) . '', WC_LOG_DIR ); - } else { - printf( '' . __( 'Log directory (%s) is not writable. To allow logging, make this writable or define a custom WC_LOG_DIR.', 'woocommerce' ) . '', WC_LOG_DIR ); - } - ?>:
    :: ' . sprintf( __( 'Default timezone is %s - it should be UTC', 'woocommerce' ), $default_timezone ) . ''; + if ( 'UTC' !== $environment['default_timezone'] ) { + echo ' ' . sprintf( __( 'Default timezone is %s - it should be UTC', 'woocommerce' ), $environment['default_timezone'] ) . ''; } else { - echo '' . sprintf( __( 'Default timezone is %s', 'woocommerce' ), $default_timezone ) . ''; + echo ''; + } ?> +
    :'; + } else { + echo ' ' . __( 'Your server does not have fsockopen or cURL enabled - PayPal IPN and other scripts which communicate with other servers will not work. Contact your hosting provider.', 'woocommerce' ) . ''; + } ?> +
    :'; + } else { + echo ' ' . sprintf( __( 'Your server does not have the %s class enabled - some gateway plugins which use SOAP may not work as expected.', 'woocommerce' ), 'SoapClient' ) . ''; + } ?> +
    :'; + } else { + echo ' ' . sprintf( __( 'Your server does not have the %s class enabled - HTML/Multipart emails, and also some extensions, will not work without DOMDocument.', 'woocommerce' ), 'DOMDocument' ) . ''; + } ?> +
    :'; + } else { + echo ' ' . sprintf( __( 'Your server does not support the %s function - this is required to use the GeoIP database from MaxMind.', 'woocommerce' ), 'gzopen' ) . ''; + } ?> +
    :'; + } else { + echo ' ' . sprintf( __( 'Your server does not support the %s functions - this is required for better character encoding. Some fallbacks will be used instead for it.', 'woocommerce' ), 'mbstring' ) . ''; + } ?> +
    :'; + } else { + echo ' ' . __( 'wp_remote_post() failed. Contact your hosting provider.', 'woocommerce' ) . ' ' . esc_html( $environment['remote_post_response'] ) . ''; + } ?> +
    :'; + } else { + echo ' ' . __( 'wp_remote_get() failed. Contact your hosting provider.', 'woocommerce' ) . ' ' . esc_html( $environment['remote_get_response'] ) . ''; } ?>
    : + + + +
    + + + + + + + + + + + + + + + + + + $table_exists ) { + ?> + + + + + + SOAP Client class enabled - some gateway plugins which use SOAP may not work as expected.', 'woocommerce' ), 'http://php.net/manual/en/class.soapclient.php' ) . ''; - $posting['soap_client']['success'] = false; - } + if ( $settings['geolocation_enabled'] ) { + ?> + + + + + + + +

    :
      20 ) { + echo ' ' . sprintf( __( '%1$s - We recommend using a prefix with less than 20 characters. See: %2$s', 'woocommerce' ), esc_html( $database['database_prefix'] ), '' . __( 'How to update your database table prefix', 'woocommerce' ) . '' ) . ''; } else { - $posting['fsockopen_curl']['note'] = __( 'Your server has cURL enabled, fsockopen is disabled.', 'woocommerce' ); + echo '' . esc_html( $database['database_prefix'] ) . ''; } - $posting['fsockopen_curl']['success'] = true; - } else { - $posting['fsockopen_curl']['note'] = __( 'Your server does not have fsockopen or cURL enabled - PayPal IPN and other scripts which communicate with other servers will not work. Contact your hosting provider.', 'woocommerce' ). ''; - $posting['fsockopen_curl']['success'] = false; - } + ?> +
      ' . __( 'Table does not exist', 'woocommerce' ) . '' : ''; ?>
    : ' . esc_html( $database['maxmind_geoip_database'] ) . ' '; + } else { + printf( ' ' . sprintf( __( 'The MaxMind GeoIP Database does not exist - Geolocation will not function. You can download and install it manually from %1$s to the path: %2$s. Scroll down to "Downloads" and download the "Binary / gzip" file next to "GeoLite Country". Please remember to uncompress GeoIP.dat.gz and upload the GeoIP.dat file only.', 'woocommerce' ), make_clickable( 'http://dev.maxmind.com/geoip/legacy/geolite/' ), '' . $database['maxmind_geoip_database'] . '' ) . '', WC_LOG_DIR ); + } + ?>
    + + + + + + + + + + + + + + + + + + +

    : + + + + Learn more about HTTPS and SSL Certificates.', 'woocommerce' ), 'https://docs.woocommerce.com/document/ssl-and-https/' ); ?> + +
    + + + + + +
    + + + + + + + + false, - 'timeout' => 60, - 'user-agent' => 'WooCommerce/' . WC()->version, - 'body' => $request - ); - $response = wp_remote_post( 'https://www.paypal.com/cgi-bin/webscr', $params ); + // Link the plugin name to the plugin url if available. + $plugin_name = esc_html( $plugin['name'] ); + if ( ! empty( $plugin['url'] ) ) { + $plugin_name = '' . $plugin_name . ''; + } - if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 ) { - $posting['wp_remote_post']['note'] = __('wp_remote_post() was successful - PayPal IPN is working.', 'woocommerce' ); - $posting['wp_remote_post']['success'] = true; - } elseif ( is_wp_error( $response ) ) { - $posting['wp_remote_post']['note'] = __( 'wp_remote_post() failed. PayPal IPN won\'t work with your server. Contact your hosting provider. Error:', 'woocommerce' ) . ' ' . $response->get_error_message(); - $posting['wp_remote_post']['success'] = false; - } else { - $posting['wp_remote_post']['note'] = __( 'wp_remote_post() failed. PayPal IPN may not work with your server.', 'woocommerce' ); - $posting['wp_remote_post']['success'] = false; - } + $version_string = ''; + $network_string = ''; + if ( strstr( $plugin['url'], 'woothemes.com' ) || strstr( $plugin['url'], 'woocommerce.com' ) ) { + if ( ! empty( $plugin['version_latest'] ) && version_compare( $plugin['version_latest'], $plugin['version'], '>' ) ) { + /* translators: %s: plugin latest version */ + $version_string = ' – ' . sprintf( esc_html__( '%s is available', 'woocommerce' ), $plugin['version_latest'] ) . ''; + } - $posting = apply_filters( 'woocommerce_debug_posting', $posting ); - - foreach( $posting as $post ) { $mark = ( isset( $post['success'] ) && $post['success'] == true ) ? 'yes' : 'error'; + if ( false != $plugin['network_activated'] ) { + $network_string = ' – ' . __( 'Network enabled', 'woocommerce' ) . ''; + } + } ?> - - + + + - +

    ()

    : - - - -  
    + - + - - - $val ) - if ( in_array( $key, array( 'decimal_point', 'mon_decimal_point', 'thousands_sep', 'mon_thousands_sep' ) ) ) - echo ''; - ?> - - - - - - - - - - + + + - - - - + + + - - - - - + + + - - - - + + + - - - - array( - 'option' => 'woocommerce_shop_page_id', - 'shortcode' => '' - ), - _x( 'Cart', 'Page setting', 'woocommerce' ) => array( - 'option' => 'woocommerce_cart_page_id', - 'shortcode' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']' - ), - _x( 'Checkout', 'Page setting', 'woocommerce' ) => array( - 'option' => 'woocommerce_checkout_page_id', - 'shortcode' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']' - ), - _x( 'My Account', 'Page setting', 'woocommerce' ) => array( - 'option' => 'woocommerce_myaccount_page_id', - 'shortcode' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']' - ) - ); - - $alt = 1; - - foreach ( $check_pages as $page_name => $values ) { - - if ( $alt == 1 ) echo ''; else echo ''; - - echo ''; - - $alt = $alt * -1; - } - ?> - - - - + + + - - - - + + + + + + + + + + + + + + + - +

    ' . $key . ':' . $val . '
    :' . $plugin_name . ''; - } - - if ( strstr( $dirname, 'woocommerce' ) ) { - - if ( false === ( $version_data = get_transient( md5( $plugin ) . '_version_data' ) ) ) { - $changelog = wp_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $dirname . '/changelog.txt' ); - $cl_lines = explode( "\n", wp_remote_retrieve_body( $changelog ) ); - if ( ! empty( $cl_lines ) ) { - foreach ( $cl_lines as $line_num => $cl_line ) { - if ( preg_match( '/^[0-9]/', $cl_line ) ) { - - $date = str_replace( '.' , '-' , trim( substr( $cl_line , 0 , strpos( $cl_line , '-' ) ) ) ); - $version = preg_replace( '~[^0-9,.]~' , '' ,stristr( $cl_line , "version" ) ); - $update = trim( str_replace( "*" , "" , $cl_lines[ $line_num + 1 ] ) ); - $version_data = array( 'date' => $date , 'version' => $version , 'update' => $update , 'changelog' => $changelog ); - set_transient( md5( $plugin ) . '_version_data', $version_data, 60*60*12 ); - break; - } - } - } - } - - if ( ! empty( $version_data['version'] ) && version_compare( $version_data['version'], $plugin_data['Version'], '>' ) ) - $version_string = ' – ' . $version_data['version'] . ' ' . __( 'is available', 'woocommerce' ) . ''; - } - - $wc_plugins[] = $plugin_name . ' ' . __( 'by', 'woocommerce' ) . ' ' . $plugin_data['Author'] . ' ' . __( 'version', 'woocommerce' ) . ' ' . $plugin_data['Version'] . $version_string; - - } - } - - if ( sizeof( $wc_plugins ) == 0 ) - echo '-'; - else - echo implode( ',
    ', $wc_plugins ); - - ?>
    :' : ''; ?>
    :' : ''; ?>
    :'.__( 'Yes', 'woocommerce' ).'' : ''.__( 'No', 'woocommerce' ).''; ?> ()
    ' . esc_html( $page_name ) . ':'; - - $error = false; - - $page_id = get_option( $values['option'] ); - - // Page ID check - if ( ! $page_id ) { - echo '' . __( 'Page not set', 'woocommerce' ) . ''; - $error = true; - } else { - - // Shortcode check - if ( $values['shortcode'] ) { - $page = get_post( $page_id ); - - if ( empty( $page ) ) { - - echo '' . sprintf( __( 'Page does not exist', 'woocommerce' ) ) . ''; - $error = true; - - } else if ( ! strstr( $page->post_content, $values['shortcode'] ) ) { - - echo '' . sprintf( __( 'Page does not contain the shortcode: %s', 'woocommerce' ), $values['shortcode'] ) . ''; - $error = true; - - } - } - - } - - if ( ! $error ) echo '#' . absint( $page_id ) . ' - ' . str_replace( home_url(), '', get_permalink( $page_id ) ) . ''; - - echo '
    :
    + 0 ) ); - foreach ( $terms as $term ) - $display_terms[] = $term->name . ' (' . $term->slug . ')'; + foreach ( $settings['taxonomies'] as $slug => $name ) { + $display_terms[] = strtolower( $name ) . ' (' . $slug . ')'; + } + echo implode( ', ', array_map( 'esc_html', $display_terms ) ); + ?>
    + $name ) { + $display_terms[] = strtolower( $name ) . ' (' . $slug . ')'; + } echo implode( ', ', array_map( 'esc_html', $display_terms ) ); ?>
    + - + - - {'Author URI'} == 'http://www.woothemes.com' ) : - - $theme_dir = substr( strtolower( str_replace( ' ','', $active_theme->Name ) ), 0, 45 ); - - if ( false === ( $theme_version_data = get_transient( $theme_dir . '_version_data' ) ) ) : - - $theme_changelog = wp_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $theme_dir . '/changelog.txt' ); - $cl_lines = explode( "\n", wp_remote_retrieve_body( $theme_changelog ) ); - if ( ! empty( $cl_lines ) ) : - - foreach ( $cl_lines as $line_num => $cl_line ) { - if ( preg_match( '/^[0-9]/', $cl_line ) ) : - - $theme_date = str_replace( '.' , '-' , trim( substr( $cl_line , 0 , strpos( $cl_line , '-' ) ) ) ); - $theme_version = preg_replace( '~[^0-9,.]~' , '' ,stristr( $cl_line , "version" ) ); - $theme_update = trim( str_replace( "*" , "" , $cl_lines[ $line_num + 1 ] ) ); - $theme_version_data = array( 'date' => $theme_date , 'version' => $theme_version , 'update' => $theme_update , 'changelog' => $theme_changelog ); - set_transient( $theme_dir . '_version_data', $theme_version_data , 60*60*12 ); - break; - - endif; - } - - endif; - - endif; - - endif; - ?> - - - - - - - - - - - - - - - - - - - - - - - - - WC()->plugin_path() . '/templates/' ) ); - $scanned_files = array(); - $found_files = array(); + if ( $page['page_id'] ) { + $page_name = '' . esc_html( $page['page_name'] ) . ''; + } else { + $page_name = esc_html( $page['page_name'] ); + } - foreach ( $template_paths as $plugin_name => $template_path ) { - $scanned_files[ $plugin_name ] = WC_Admin_Status::scan_template_files( $template_path ); - } + echo ''; + echo ''; + } + ?> + +

    :Name; - ?>
    :Version; - - if ( ! empty( $theme_version_data['version'] ) && version_compare( $theme_version_data['version'], $active_theme->Version, '!=' ) ) - echo ' – ' . $theme_version_data['version'] . ' ' . __( 'is available', 'woocommerce' ) . ''; - ?>
    :{'Author URI'}; - ?>
    :template, wc_get_core_supported_themes() ) ) { - echo '' . __( 'Not Declared', 'woocommerce' ) . ''; - } else { - echo '' . __( 'Yes', 'woocommerce' ) . ''; - } - ?>
    ' . $page_name . ':' . wc_help_tip( sprintf( __( 'The URL of your %s page (along with the Page ID).', 'woocommerce' ), $page_name ) ) . ''; - foreach ( $scanned_files as $plugin_name => $files ) { - foreach ( $files as $file ) { - if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { - $theme_file = get_stylesheet_directory() . '/' . $file; - } elseif ( file_exists( get_stylesheet_directory() . '/woocommerce/' . $file ) ) { - $theme_file = get_stylesheet_directory() . '/woocommerce/' . $file; - } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { - $theme_file = get_template_directory() . '/' . $file; - } elseif( file_exists( get_template_directory() . '/woocommerce/' . $file ) ) { - $theme_file = get_template_directory() . '/woocommerce/' . $file; - } else { - $theme_file = false; - } - - if ( $theme_file ) { - $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file ); - $theme_version = WC_Admin_Status::get_file_version( $theme_file ); - - if ( $core_version && ( empty( $theme_version ) || version_compare( $theme_version, $core_version, '<' ) ) ) { - $found_files[ $plugin_name ][] = sprintf( __( '%s version %s is out of date. The core version is %s', 'woocommerce' ), basename( $theme_file ), $theme_version ? $theme_version : '-', $core_version ); - } else { - $found_files[ $plugin_name ][] = sprintf( '%s', basename( $theme_file ) ); + // Page ID check. + if ( ! $page['page_set'] ) { + echo ' ' . __( 'Page not set', 'woocommerce' ) . ''; + $error = true; + } elseif ( ! $page['page_exists'] ) { + echo ' ' . __( 'Page ID is set, but the page does not exist', 'woocommerce' ) . ''; + $error = true; + } elseif ( ! $page['page_visible'] ) { + echo ' ' . sprintf( __( 'Page visibility should be public', 'woocommerce' ), 'https://codex.wordpress.org/Content_Visibility' ) . ''; + $error = true; + } else { + // Shortcode check + if ( $page['shortcode_required'] ) { + if ( ! $page['shortcode_present'] ) { + echo ' ' . sprintf( __( 'Page does not contain the shortcode.', 'woocommerce' ), $page['shortcode'] ) . ''; + $error = true; } } } - } - if ( $found_files ) { - foreach ( $found_files as $plugin_name => $found_plugin_files ) { - ?> + if ( ! $error ) { + echo '#' . absint( $page['page_id'] ) . ' - ' . str_replace( home_url(), '', get_permalink( $page['page_id'] ) ) . ''; + } + + echo '
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    :
    :' . sprintf( __( '%s is available', 'woocommerce' ), esc_html( $theme['version_latest'] ) ) . ''; + } + ?>
    :
    :' : ' – ' . sprintf( __( 'If you are modifying WooCommerce on a parent theme that you did not build personally we recommend using a child theme. See: How to create a child theme', 'woocommerce' ), 'https://codex.wordpress.org/Child_Themes' ); + ?>
    :
    :' . sprintf( __( '%s is available', 'woocommerce' ), esc_html( $theme['parent_version_latest'] ) ) . ''; + } + ?>
    :
    : + ' . __( 'Not declared', 'woocommerce' ) . ''; + } else { + echo ''; + } ?> +
    + + + + + + + + + + + + + + + - - + + + - - + + + + + + + + + -

    : 
    ():', $found_plugin_files ); ?>  + ' . $override['file'] . '', + '' . $current_version . '', + $override['core_version'] + ); + } else { + echo esc_html( $override['file'] ); + } + if ( ( count( $theme['overrides'] ) - 1 ) !== $i ) { + echo ', '; + } + echo '
    '; + } + ?> +
    :: 
    : 
    - + diff --git a/includes/admin/views/html-admin-page-status-tools.php b/includes/admin/views/html-admin-page-status-tools.php index abf1b7a2374..9c58c3dd957 100644 --- a/includes/admin/views/html-admin-page-status-tools.php +++ b/includes/admin/views/html-admin-page-status-tools.php @@ -1,60 +1,31 @@ +
    - 0, 'template_debug_mode' => 0, 'shipping_debug_mode' => 0 ) ); ?> - - - - - $tool ) : ?> - + - - - - - - - - - - - -

    - +

    -

    - -

    -

    - -

    -
    -

    - -

    -

    - -

    -
    -

    - -

    -

    - Delete.', 'woocommerce' ); ?> -

    -
    -

    - -

    +

    + +

    diff --git a/includes/admin/views/html-admin-page-status.php b/includes/admin/views/html-admin-page-status.php index b26718a728d..911cbb370ba 100644 --- a/includes/admin/views/html-admin-page-status.php +++ b/includes/admin/views/html-admin-page-status.php @@ -1,18 +1,33 @@ + __( 'System status', 'woocommerce' ), + 'tools' => __( 'Tools', 'woocommerce' ), + 'logs' => __( 'Logs', 'woocommerce' ), +); +$tabs = apply_filters( 'woocommerce_admin_status_tabs', $tabs ); +?>
    -


    + +

    -
    \ No newline at end of file +
    diff --git a/includes/admin/views/html-admin-settings.php b/includes/admin/views/html-admin-settings.php index 1b611bb3451..a4244710bc5 100644 --- a/includes/admin/views/html-admin-settings.php +++ b/includes/admin/views/html-admin-settings.php @@ -1,26 +1,36 @@ -
    -
    -

    - + +

    - -

    - - - - - -

    +

    + + + + +

    -
    \ No newline at end of file +
    diff --git a/includes/admin/views/html-bulk-edit-product.php b/includes/admin/views/html-bulk-edit-product.php index ce80ee1bb40..c6cdc3eb852 100644 --- a/includes/admin/views/html-bulk-edit-product.php +++ b/includes/admin/views/html-bulk-edit-product.php @@ -1,122 +1,137 @@ + +
    -

    +

    - +
    -
    - + + - + foreach ( $options as $key => $value ) { + echo ''; + } + ?> + + + +
    -
    @@ -124,60 +139,75 @@
    -
    - - - -
    +
    + + - +
    diff --git a/includes/admin/views/html-notice-legacy-shipping.php b/includes/admin/views/html-notice-legacy-shipping.php new file mode 100644 index 00000000000..19bf819b67e --- /dev/null +++ b/includes/admin/views/html-notice-legacy-shipping.php @@ -0,0 +1,22 @@ + +
    + + +

    +

    They will be removed in future versions of WooCommerce. We recommend disabling these and setting up new rates within shipping zones as soon as possible.', 'woocommerce' ); ?>

    + +

    + + + + +

    +
    diff --git a/includes/admin/views/html-notice-no-shipping-methods.php b/includes/admin/views/html-notice-no-shipping-methods.php new file mode 100644 index 00000000000..2c0e95ba7f5 --- /dev/null +++ b/includes/admin/views/html-notice-no-shipping-methods.php @@ -0,0 +1,21 @@ + +
    + + +

    +

    +

    + +

    + + +

    +
    diff --git a/includes/admin/views/html-notice-simplify-commerce.php b/includes/admin/views/html-notice-simplify-commerce.php new file mode 100644 index 00000000000..8664b6b270a --- /dev/null +++ b/includes/admin/views/html-notice-simplify-commerce.php @@ -0,0 +1,24 @@ + +
    + + +

    The Simplify Commerce payment gateway is deprecated – Please install our new free Simplify Commerce plugin from WordPress.org. Simplify Commerce will be removed from WooCommerce core in a future update.', 'woocommerce' ); ?>

    + +

    +
    diff --git a/includes/admin/views/html-notice-template-check.php b/includes/admin/views/html-notice-template-check.php index ac05441796b..e326a750461 100644 --- a/includes/admin/views/html-notice-template-check.php +++ b/includes/admin/views/html-notice-template-check.php @@ -1,7 +1,17 @@ -
    -

    Your theme has bundled outdated copies of WooCommerce template files – if you encounter functionality issues on the frontend this could the reason. Ensure you update or remove them (in general we recommend only bundling the template files you actually need to customize). See the system report for full details.', 'woocommerce' ); ?>

    -

    -
    \ No newline at end of file +
    + + +

    Your theme (%1$s) contains outdated copies of some WooCommerce template files. These files may need updating to ensure they are compatible with the current version of WooCommerce. You can see which files are affected from the system status page. If in doubt, check with the author of the theme.', 'woocommerce' ), esc_html( $theme['Name'] ), esc_url( admin_url( 'admin.php?page=wc-status' ) ) ); ?>

    +

    +
    diff --git a/includes/admin/views/html-notice-theme-support.php b/includes/admin/views/html-notice-theme-support.php index e38121cf68b..37987b27439 100644 --- a/includes/admin/views/html-notice-theme-support.php +++ b/includes/admin/views/html-notice-theme-support.php @@ -1,7 +1,19 @@
    -

    Your theme does not declare WooCommerce support – if you encounter layout issues please read our integration guide or choose a WooCommerce theme :)', 'woocommerce' ); ?>

    -

    -
    \ No newline at end of file + + +

    Your theme does not declare WooCommerce support – Please read our integration guide or check out our Storefront theme which is totally free to download and designed specifically for use with WooCommerce.', 'woocommerce' ), esc_url( apply_filters( 'woocommerce_docs_url', 'https://docs.woocommerce.com/document/third-party-custom-theme-compatibility/', 'theme-compatibility' ) ), esc_url( admin_url( 'theme-install.php?theme=storefront' ) ) ); ?>

    +

    + + +

    +
    diff --git a/includes/admin/views/html-notice-tracking.php b/includes/admin/views/html-notice-tracking.php new file mode 100644 index 00000000000..5ba3298d386 --- /dev/null +++ b/includes/admin/views/html-notice-tracking.php @@ -0,0 +1,16 @@ + +
    +

    Find out more.', 'woocommerce' ), '20%', 'https://woocommerce.com/usage-tracking/' ); ?>

    +

    + + +

    +
    diff --git a/includes/admin/views/html-notice-translation-upgrade.php b/includes/admin/views/html-notice-translation-upgrade.php deleted file mode 100644 index 200fbb67539..00000000000 --- a/includes/admin/views/html-notice-translation-upgrade.php +++ /dev/null @@ -1,9 +0,0 @@ - -
    -

    WooCommerce Translation Available – Install or update your %s translation to version %s.', 'woocommerce' ), WC_Language_Pack_Upgrader::get_language(), WC_VERSION ); ?>

    -

    -
    diff --git a/includes/admin/views/html-notice-update.php b/includes/admin/views/html-notice-update.php index 823a05389c0..f41c1674c5e 100644 --- a/includes/admin/views/html-notice-update.php +++ b/includes/admin/views/html-notice-update.php @@ -1,13 +1,19 @@
    -

    WooCommerce Data Update Required – We just need to update your install to the latest version', 'woocommerce' ); ?>

    -

    +

    +

    \ No newline at end of file + diff --git a/includes/admin/views/html-notice-updated.php b/includes/admin/views/html-notice-updated.php new file mode 100644 index 00000000000..03d81f7164d --- /dev/null +++ b/includes/admin/views/html-notice-updated.php @@ -0,0 +1,15 @@ + +
    + + +

    +
    diff --git a/includes/admin/views/html-notice-updating.php b/includes/admin/views/html-notice-updating.php new file mode 100644 index 00000000000..b77d86741ab --- /dev/null +++ b/includes/admin/views/html-notice-updating.php @@ -0,0 +1,13 @@ + +
    +

    +
    diff --git a/includes/admin/views/html-quick-edit-product.php b/includes/admin/views/html-quick-edit-product.php index df0d69b4dce..8e7144a5066 100644 --- a/includes/admin/views/html-quick-edit-product.php +++ b/includes/admin/views/html-quick-edit-product.php @@ -1,15 +1,26 @@ + +
    -

    +

    @@ -19,71 +30,75 @@


    - -
    - +
    + -
    + foreach ( $options as $key => $value ) { + echo ''; + } + ?> + + + +
    +

    @@ -92,11 +107,11 @@
    - - - - - + + + + +
    @@ -106,18 +121,33 @@ +
    + +
    -
    \ No newline at end of file + diff --git a/includes/admin/views/html-report-by-date.php b/includes/admin/views/html-report-by-date.php index d99d9ea1ec7..fc68eb33f47 100644 --- a/includes/admin/views/html-report-by-date.php +++ b/includes/admin/views/html-report-by-date.php @@ -1,46 +1,73 @@ +
    -

    + + +

    + +

    + + +
    get_export_button(); ?>
      $name ) - echo '
    • ' . $name . '
    • '; + foreach ( $ranges as $range => $name ) { + echo '
    • ' . $name . '
    • '; + } ?> -
    • +
    • $value ) - if ( is_array( $value ) ) - foreach ( $value as $v ) + foreach ( $_GET as $key => $value ) { + if ( is_array( $value ) ) { + foreach ( $value as $v ) { echo ''; - else + } + } else { echo ''; + } + } ?> - - - + + + + +
    - +
    get_chart_legend() ) : ?>
      -
    • > + +
    • data-tip="">
    • @@ -65,4 +92,4 @@
    -
    \ No newline at end of file +
    diff --git a/includes/admin/wc-admin-functions.php b/includes/admin/wc-admin-functions.php index 0b577cac006..a8f77be4eaf 100644 --- a/includes/admin/wc-admin-functions.php +++ b/includes/admin/wc-admin-functions.php @@ -2,93 +2,133 @@ /** * WooCommerce Admin Functions * - * @author WooThemes - * @category Core - * @package WooCommerce/Admin/Functions - * @version 2.1.0 + * @author WooThemes + * @category Core + * @package WooCommerce/Admin/Functions + * @version 2.4.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly +} /** - * Get all WooCommerce screen ids + * Get all WooCommerce screen ids. * * @return array */ function wc_get_screen_ids() { - $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); - return apply_filters( 'woocommerce_screen_ids', array( - 'toplevel_page_' . $wc_screen_id, - $wc_screen_id . '_page_wc-reports', - $wc_screen_id . '_page_wc-settings', - $wc_screen_id . '_page_wc-status', - $wc_screen_id . '_page_wc-addons', - 'product_page_product_attributes', - 'edit-shop_order', - 'shop_order', - 'edit-product', - 'product', - 'edit-shop_coupon', - 'shop_coupon', - 'edit-product_cat', - 'edit-product_tag', - 'edit-product_shipping_class' - ) ); + $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + $screen_ids = array( + 'toplevel_page_' . $wc_screen_id, + $wc_screen_id . '_page_wc-reports', + $wc_screen_id . '_page_wc-shipping', + $wc_screen_id . '_page_wc-settings', + $wc_screen_id . '_page_wc-status', + $wc_screen_id . '_page_wc-addons', + 'toplevel_page_wc-reports', + 'product_page_product_attributes', + 'product_page_product_exporter', + 'product_page_product_importer', + 'edit-product', + 'product', + 'edit-shop_coupon', + 'shop_coupon', + 'edit-product_cat', + 'edit-product_tag', + 'profile', + 'user-edit', + ); + + foreach ( wc_get_order_types() as $type ) { + $screen_ids[] = $type; + $screen_ids[] = 'edit-' . $type; + } + + if ( $attributes = wc_get_attribute_taxonomies() ) { + foreach ( $attributes as $attribute ) { + $screen_ids[] = 'edit-' . wc_attribute_taxonomy_name( $attribute->attribute_name ); + } + } + + return apply_filters( 'woocommerce_screen_ids', $screen_ids ); } /** * Create a page and store the ID in an option. * - * @access public * @param mixed $slug Slug for the new page - * @param mixed $option Option name to store the page's ID + * @param string $option Option name to store the page's ID * @param string $page_title (default: '') Title for the new page * @param string $page_content (default: '') Content for the new page * @param int $post_parent (default: 0) Parent for the new page * @return int page ID */ function wc_create_page( $slug, $option = '', $page_title = '', $page_content = '', $post_parent = 0 ) { - global $wpdb; + global $wpdb; - $option_value = get_option( $option ); + $option_value = get_option( $option ); - if ( $option_value > 0 && get_post( $option_value ) ) - return -1; + if ( $option_value > 0 && ( $page_object = get_post( $option_value ) ) ) { + if ( 'page' === $page_object->post_type && ! in_array( $page_object->post_status, array( 'pending', 'trash', 'future', 'auto-draft' ) ) ) { + // Valid page is already in place + return $page_object->ID; + } + } - $page_found = null; + if ( strlen( $page_content ) > 0 ) { + // Search for an existing page with the specified page content (typically a shortcode) + $valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_content LIKE %s LIMIT 1;", "%{$page_content}%" ) ); + } else { + // Search for an existing page with the specified page slug + $valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_name = %s LIMIT 1;", $slug ) ); + } - if ( strlen( $page_content ) > 0 ) { - // Search for an existing page with the specified page content (typically a shortcode) - $page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM " . $wpdb->posts . " WHERE post_type='page' AND post_content LIKE %s LIMIT 1;", "%{$page_content}%" ) ); - } else { - // Search for an existing page with the specified page slug - $page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM " . $wpdb->posts . " WHERE post_type='page' AND post_name = %s LIMIT 1;", $slug ) ); - } + $valid_page_found = apply_filters( 'woocommerce_create_page_id', $valid_page_found, $slug, $page_content ); - if ( $page_found ) { - if ( ! $option_value ) - update_option( $option, $page_found ); - - return $page_found; - } + if ( $valid_page_found ) { + if ( $option ) { + update_option( $option, $valid_page_found ); + } + return $valid_page_found; + } - $page_data = array( - 'post_status' => 'publish', - 'post_type' => 'page', - 'post_author' => 1, - 'post_name' => $slug, - 'post_title' => $page_title, - 'post_content' => $page_content, - 'post_parent' => $post_parent, - 'comment_status' => 'closed' - ); - $page_id = wp_insert_post( $page_data ); + // Search for a matching valid trashed page + if ( strlen( $page_content ) > 0 ) { + // Search for an existing page with the specified page content (typically a shortcode) + $trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_content LIKE %s LIMIT 1;", "%{$page_content}%" ) ); + } else { + // Search for an existing page with the specified page slug + $trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_name = %s LIMIT 1;", $slug ) ); + } - if ( $option ) - update_option( $option, $page_id ); + if ( $trashed_page_found ) { + $page_id = $trashed_page_found; + $page_data = array( + 'ID' => $page_id, + 'post_status' => 'publish', + ); + wp_update_post( $page_data ); + } else { + $page_data = array( + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_author' => 1, + 'post_name' => $slug, + 'post_title' => $page_title, + 'post_content' => $page_content, + 'post_parent' => $post_parent, + 'comment_status' => 'closed', + ); + $page_id = wp_insert_post( $page_data ); + } - return $page_id; + if ( $option ) { + update_option( $option, $page_id ); + } + + return $page_id; } /** @@ -99,95 +139,172 @@ function wc_create_page( $slug, $option = '', $page_title = '', $page_content = * @param array $options Opens array to output */ function woocommerce_admin_fields( $options ) { - if ( ! class_exists( 'WC_Admin_Settings' ) ) - include 'class-wc-admin-settings.php'; - WC_Admin_Settings::output_fields( $options ); + if ( ! class_exists( 'WC_Admin_Settings', false ) ) { + include( dirname( __FILE__ ) . '/class-wc-admin-settings.php' ); + } + + WC_Admin_Settings::output_fields( $options ); } /** * Update all settings which are passed. * - * @access public * @param array $options - * @return void + * @param array $data */ -function woocommerce_update_options( $options ) { - if ( ! class_exists( 'WC_Admin_Settings' ) ) - include 'class-wc-admin-settings.php'; +function woocommerce_update_options( $options, $data = null ) { - WC_Admin_Settings::save_fields( $options ); + if ( ! class_exists( 'WC_Admin_Settings', false ) ) { + include( dirname( __FILE__ ) . '/class-wc-admin-settings.php' ); + } + + WC_Admin_Settings::save_fields( $options, $data ); } /** * Get a setting from the settings API. * - * @param mixed $option + * @param mixed $option_name + * @param mixed $default * @return string */ function woocommerce_settings_get_option( $option_name, $default = '' ) { - if ( ! class_exists( 'WC_Admin_Settings' ) ) - include 'class-wc-admin-settings.php'; - return WC_Admin_Settings::get_option( $option_name, $default ); + if ( ! class_exists( 'WC_Admin_Settings', false ) ) { + include( dirname( __FILE__ ) . '/class-wc-admin-settings.php' ); + } + + return WC_Admin_Settings::get_option( $option_name, $default ); } /** - * Generate CSS from the less file when changing colours. + * Save order items. Uses the CRUD. * - * @access public - * @return void + * @since 2.2 + * @param int $order_id Order ID + * @param array $items Order items to save */ -function woocommerce_compile_less_styles() { - $colors = array_map( 'esc_attr', (array) get_option( 'woocommerce_frontend_css_colors' ) ); - $base_file = WC()->plugin_path() . '/assets/css/woocommerce-base.less'; - $less_file = WC()->plugin_path() . '/assets/css/woocommerce.less'; - $css_file = WC()->plugin_path() . '/assets/css/woocommerce.css'; +function wc_save_order_items( $order_id, $items ) { + // Allow other plugins to check change in order items before they are saved + do_action( 'woocommerce_before_save_order_items', $order_id, $items ); - // Write less file - if ( is_writable( $base_file ) && is_writable( $css_file ) ) { + $order = wc_get_order( $order_id ); - // Colours changed - recompile less - if ( ! class_exists( 'lessc' ) ) - include_once( WC()->plugin_path() . '/includes/libraries/class-lessc.php' ); - if ( ! class_exists( 'cssmin' ) ) - include_once( WC()->plugin_path() . '/includes/libraries/class-cssmin.php' ); + // Line items and fees + if ( isset( $items['order_item_id'] ) ) { + $data_keys = array( + 'line_tax' => array(), + 'line_subtotal_tax' => array(), + 'order_item_name' => null, + 'order_item_qty' => null, + 'order_item_tax_class' => null, + 'line_total' => null, + 'line_subtotal' => null, + ); + foreach ( $items['order_item_id'] as $item_id ) { + if ( ! $item = $order->get_item( absint( $item_id ) ) ) { + continue; + } - try { - // Set default if colours not set - if ( ! $colors['primary'] ) $colors['primary'] = '#ad74a2'; - if ( ! $colors['secondary'] ) $colors['secondary'] = '#f7f6f7'; - if ( ! $colors['highlight'] ) $colors['highlight'] = '#85ad74'; - if ( ! $colors['content_bg'] ) $colors['content_bg'] = '#ffffff'; - if ( ! $colors['subtext'] ) $colors['subtext'] = '#777777'; + $item_data = array(); - // Write new color to base file - $color_rules = " -@primary: " . $colors['primary'] . "; -@primarytext: " . wc_light_or_dark( $colors['primary'], 'desaturate(darken(@primary,50%),18%)', 'desaturate(lighten(@primary,50%),18%)' ) . "; + foreach ( $data_keys as $key => $default ) { + $item_data[ $key ] = isset( $items[ $key ][ $item_id ] ) ? wc_clean( wp_unslash( $items[ $key ][ $item_id ] ) ) : $default; + } -@secondary: " . $colors['secondary'] . "; -@secondarytext: " . wc_light_or_dark( $colors['secondary'], 'desaturate(darken(@secondary,60%),18%)', 'desaturate(lighten(@secondary,60%),18%)' ) . "; + if ( '0' === $item_data['order_item_qty'] ) { + $item->delete(); + continue; + } -@highlight: " . $colors['highlight'] . "; -@highlightext: " . wc_light_or_dark( $colors['highlight'], 'desaturate(darken(@highlight,60%),18%)', 'desaturate(lighten(@highlight,60%),18%)' ) . "; + $item->set_props( array( + 'name' => $item_data['order_item_name'], + 'quantity' => $item_data['order_item_qty'], + 'tax_class' => $item_data['order_item_tax_class'], + 'total' => $item_data['line_total'], + 'subtotal' => $item_data['line_subtotal'], + 'taxes' => array( + 'total' => $item_data['line_tax'], + 'subtotal' => $item_data['line_subtotal_tax'], + ), + ) ); -@contentbg: " . $colors['content_bg'] . "; + if ( isset( $items['meta_key'][ $item_id ], $items['meta_value'][ $item_id ] ) ) { + foreach ( $items['meta_key'][ $item_id ] as $meta_id => $meta_key ) { + $meta_value = isset( $items['meta_value'][ $item_id ][ $meta_id ] ) ? wp_unslash( $items['meta_value'][ $item_id ][ $meta_id ] ) : ''; -@subtext: " . $colors['subtext'] . "; - "; + if ( '' === $meta_key && '' === $meta_value ) { + if ( ! strstr( $meta_id, 'new-' ) ) { + $item->delete_meta_data_by_mid( $meta_id ); + } + } elseif ( strstr( $meta_id, 'new-' ) ) { + $item->add_meta_data( $meta_key, $meta_value, false ); + } else { + $item->update_meta_data( $meta_key, $meta_value, $meta_id ); + } + } + } - file_put_contents( $base_file, $color_rules ); + $item->save(); + } + } - $less = new lessc; - $compiled_css = $less->compileFile( $less_file ); - $compiled_css = CssMin::minify( $compiled_css ); + // Shipping Rows + if ( isset( $items['shipping_method_id'] ) ) { + $data_keys = array( + 'shipping_method' => null, + 'shipping_method_title' => null, + 'shipping_cost' => 0, + 'shipping_taxes' => array(), + ); - if ( $compiled_css ) - file_put_contents( $css_file, $compiled_css ); + foreach ( $items['shipping_method_id'] as $item_id ) { + if ( ! $item = $order->get_item( absint( $item_id ) ) ) { + continue; + } - } catch ( exception $ex ) { - wp_die( __( 'Could not compile woocommerce.less:', 'woocommerce' ) . ' ' . $ex->getMessage() ); - } - } + $item_data = array(); + + foreach ( $data_keys as $key => $default ) { + $item_data[ $key ] = isset( $items[ $key ][ $item_id ] ) ? wc_clean( wp_unslash( $items[ $key ][ $item_id ] ) ) : $default; + } + + $item->set_props( array( + 'method_id' => $item_data['shipping_method'], + 'method_title' => $item_data['shipping_method_title'], + 'total' => $item_data['shipping_cost'], + 'taxes' => array( + 'total' => $item_data['shipping_taxes'], + ), + ) ); + + if ( isset( $items['meta_key'][ $item_id ], $items['meta_value'][ $item_id ] ) ) { + foreach ( $items['meta_key'][ $item_id ] as $meta_id => $meta_key ) { + $meta_value = isset( $items['meta_value'][ $item_id ][ $meta_id ] ) ? wp_unslash( $items['meta_value'][ $item_id ][ $meta_id ] ) : ''; + + if ( '' === $meta_key && '' === $meta_value ) { + if ( ! strstr( $meta_id, 'new-' ) ) { + $item->delete_meta_data_by_mid( $meta_id ); + } + } elseif ( strstr( $meta_id, 'new-' ) ) { + $item->add_meta_data( $meta_key, $meta_value, false ); + } else { + $item->update_meta_data( $meta_key, $meta_value, $meta_id ); + } + } + } + + $item->save(); + } + } + + // Updates tax totals + $order->update_taxes(); + + // Calc totals - this also triggers save + $order->calculate_totals( false ); + + // Inform other plugins that the items have been saved + do_action( 'woocommerce_saved_order_items', $order_id, $items ); } diff --git a/includes/admin/wc-meta-box-functions.php b/includes/admin/wc-meta-box-functions.php index ab9a9d39a9c..a429735979d 100644 --- a/includes/admin/wc-meta-box-functions.php +++ b/includes/admin/wc-meta-box-functions.php @@ -1,21 +1,20 @@ ID : $thepostid; $field['placeholder'] = isset( $field['placeholder'] ) ? $field['placeholder'] : ''; $field['class'] = isset( $field['class'] ) ? $field['class'] : 'short'; + $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; $field['type'] = isset( $field['type'] ) ? $field['type'] : 'text'; + $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; $data_type = empty( $field['data_type'] ) ? '' : $field['data_type']; switch ( $data_type ) { case 'price' : $field['class'] .= ' wc_input_price'; $field['value'] = wc_format_localized_price( $field['value'] ); - break; + break; case 'decimal' : $field['class'] .= ' wc_input_decimal'; $field['value'] = wc_format_localized_decimal( $field['value'] ); - break; + break; + case 'stock' : + $field['class'] .= ' wc_input_stock'; + $field['value'] = wc_stock_amount( $field['value'] ); + break; + case 'url' : + $field['class'] .= ' wc_input_url'; + $field['value'] = esc_url( $field['value'] ); + break; + + default : + break; } // Custom attribute handling $custom_attributes = array(); - if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) - foreach ( $field['custom_attributes'] as $attribute => $value ) + if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) { + + foreach ( $field['custom_attributes'] as $attribute => $value ) { $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"'; - - echo '

    '; - - if ( ! empty( $field['description'] ) ) { - - if ( isset( $field['desc_tip'] ) && false !== $field['desc_tip'] ) { - echo ''; - } else { - echo '' . wp_kses_post( $field['description'] ) . ''; } - } + + echo '

    + '; + + if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) { + echo wc_help_tip( $field['description'] ); + } + + echo ' '; + + if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { + echo '' . wp_kses_post( $field['description'] ) . ''; + } + echo '

    '; } /** * Output a hidden input box. * - * @access public * @param array $field - * @return void */ function woocommerce_wp_hidden_input( $field ) { global $thepostid, $post; @@ -75,59 +90,93 @@ function woocommerce_wp_hidden_input( $field ) { $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['class'] = isset( $field['class'] ) ? $field['class'] : ''; - echo ' '; + echo ' '; } /** * Output a textarea input box. * - * @access public * @param array $field - * @return void */ function woocommerce_wp_textarea_input( $field ) { global $thepostid, $post; - $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; - $field['placeholder'] = isset( $field['placeholder'] ) ? $field['placeholder'] : ''; - $field['class'] = isset( $field['class'] ) ? $field['class'] : 'short'; + $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; + $field['placeholder'] = isset( $field['placeholder'] ) ? $field['placeholder'] : ''; + $field['class'] = isset( $field['class'] ) ? $field['class'] : 'short'; + $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; - $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); + $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); + $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; + $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; + $field['rows'] = isset( $field['rows'] ) ? $field['rows'] : 2; + $field['cols'] = isset( $field['cols'] ) ? $field['cols'] : 20; - echo '

    '; + // Custom attribute handling + $custom_attributes = array(); - if ( ! empty( $field['description'] ) ) { + if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) { - if ( isset( $field['desc_tip'] ) && false !== $field['desc_tip'] ) { - echo ''; - } else { - echo '' . wp_kses_post( $field['description'] ) . ''; + foreach ( $field['custom_attributes'] as $attribute => $value ) { + $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"'; } - } + + echo '

    + '; + + if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) { + echo wc_help_tip( $field['description'] ); + } + + echo ' '; + + if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { + echo '' . wp_kses_post( $field['description'] ) . ''; + } + echo '

    '; } /** * Output a checkbox input box. * - * @access public * @param array $field - * @return void */ function woocommerce_wp_checkbox( $field ) { global $thepostid, $post; $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; $field['class'] = isset( $field['class'] ) ? $field['class'] : 'checkbox'; + $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['cbvalue'] = isset( $field['cbvalue'] ) ? $field['cbvalue'] : 'yes'; $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; + $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; - echo '

    '; + // Custom attribute handling + $custom_attributes = array(); - if ( ! empty( $field['description'] ) ) echo '' . wp_kses_post( $field['description'] ) . ''; + if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) { + + foreach ( $field['custom_attributes'] as $attribute => $value ) { + $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"'; + } + } + + echo '

    + '; + + if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) { + echo wc_help_tip( $field['description'] ); + } + + echo ' '; + + if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { + echo '' . wp_kses_post( $field['description'] ) . ''; + } echo '

    '; } @@ -135,80 +184,92 @@ function woocommerce_wp_checkbox( $field ) { /** * Output a select input box. * - * @access public * @param array $field - * @return void */ function woocommerce_wp_select( $field ) { global $thepostid, $post; - $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; - $field['class'] = isset( $field['class'] ) ? $field['class'] : 'select short'; + $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; + $field['class'] = isset( $field['class'] ) ? $field['class'] : 'select short'; + $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; - $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); + $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); + $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; + $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; - echo '

    '; foreach ( $field['options'] as $key => $value ) { - echo ''; - } echo ' '; - if ( ! empty( $field['description'] ) ) { - - if ( isset( $field['desc_tip'] ) && false !== $field['desc_tip'] ) { - echo ''; - } else { - echo '' . wp_kses_post( $field['description'] ) . ''; - } - + if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { + echo '' . wp_kses_post( $field['description'] ) . ''; } + echo '

    '; } /** * Output a radio input box. * - * @access public * @param array $field - * @return void */ function woocommerce_wp_radio( $field ) { global $thepostid, $post; - $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; - $field['class'] = isset( $field['class'] ) ? $field['class'] : 'select short'; + $thepostid = empty( $thepostid ) ? $post->ID : $thepostid; + $field['class'] = isset( $field['class'] ) ? $field['class'] : 'select short'; + $field['style'] = isset( $field['style'] ) ? $field['style'] : ''; $field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : ''; - $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); + $field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true ); $field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id']; + $field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false; - echo '
    ' . wp_kses_post( $field['label'] ) . '
      '; + echo '
      ' . wp_kses_post( $field['label'] ) . ''; - foreach ( $field['options'] as $key => $value ) { + if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) { + echo wc_help_tip( $field['description'] ); + } + + echo '
        '; + + foreach ( $field['options'] as $key => $value ) { echo '
      • -
      • '; + name="' . esc_attr( $field['name'] ) . '" + value="' . esc_attr( $key ) . '" + type="radio" + class="' . esc_attr( $field['class'] ) . '" + style="' . esc_attr( $field['style'] ) . '" + ' . checked( esc_attr( $field['value'] ), esc_attr( $key ), false ) . ' + /> ' . esc_html( $value ) . ' + '; } - echo '
      '; - - if ( ! empty( $field['description'] ) ) { - - if ( isset( $field['desc_tip'] ) && false !== $field['desc_tip'] ) { - echo ''; - } else { - echo '' . wp_kses_post( $field['description'] ) . ''; - } + echo '
    '; + if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) { + echo '' . wp_kses_post( $field['description'] ) . ''; } - echo '
    '; -} \ No newline at end of file + echo ''; +} diff --git a/includes/api/class-wc-api-authentication.php b/includes/api/class-wc-api-authentication.php deleted file mode 100644 index 7020a6f68a4..00000000000 --- a/includes/api/class-wc-api-authentication.php +++ /dev/null @@ -1,354 +0,0 @@ -api->server->path ) - return new WP_User(0); - - try { - - if ( is_ssl() ) - $user = $this->perform_ssl_authentication(); - else - $user = $this->perform_oauth_authentication(); - - // check API key-specific permission - $this->check_api_key_permissions( $user ); - - } catch ( Exception $e ) { - - $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - - return $user; - } - - /** - * SSL-encrypted requests are not subject to sniffing or man-in-the-middle - * attacks, so the request can be authenticated by simply looking up the user - * associated with the given consumer key and confirming the consumer secret - * provided is valid - * - * @since 2.1 - * @return WP_User - * @throws Exception - */ - private function perform_ssl_authentication() { - - $params = WC()->api->server->params['GET']; - - // get consumer key - if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) { - - // should be in HTTP Auth header by default - $consumer_key = $_SERVER['PHP_AUTH_USER']; - - } elseif ( ! empty( $params['consumer_key'] ) ) { - - // allow a query string parameter as a fallback - $consumer_key = $params['consumer_key']; - - } else { - - throw new Exception( __( 'Consumer Key is missing', 'woocommerce' ), 404 ); - } - - // get consumer secret - if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { - - // should be in HTTP Auth header by default - $consumer_secret = $_SERVER['PHP_AUTH_PW']; - - } elseif ( ! empty( $params['consumer_secret'] ) ) { - - // allow a query string parameter as a fallback - $consumer_secret = $params['consumer_secret']; - - } else { - - throw new Exception( __( 'Consumer Secret is missing', 'woocommerce' ), 404 ); - } - - $user = $this->get_user_by_consumer_key( $consumer_key ); - - if ( ! $this->is_consumer_secret_valid( $user, $consumer_secret ) ) { - throw new Exception( __( 'Consumer Secret is invalid', 'woocommerce' ), 401 ); - } - - return $user; - } - - /** - * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests - * - * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP - * - * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: - * - * 1) There is no token associated with request/responses, only consumer keys/secrets are used - * - * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, - * This is because there is no cross-OS function within PHP to get the raw Authorization header - * - * @link http://tools.ietf.org/html/rfc5849 for the full spec - * @since 2.1 - * @return WP_User - * @throws Exception - */ - private function perform_oauth_authentication() { - - $params = WC()->api->server->params['GET']; - - $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); - - // check for required OAuth parameters - foreach ( $param_names as $param_name ) { - - if ( empty( $params[ $param_name ] ) ) - throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 ); - } - - // fetch WP user by consumer key - $user = $this->get_user_by_consumer_key( $params['oauth_consumer_key'] ); - - // perform OAuth validation - $this->check_oauth_signature( $user, $params ); - $this->check_oauth_timestamp_and_nonce( $user, $params['oauth_timestamp'], $params['oauth_nonce'] ); - - // authentication successful, return user - return $user; - } - - /** - * Return the user for the given consumer key - * - * @since 2.1 - * @param string $consumer_key - * @return WP_User - * @throws Exception - */ - private function get_user_by_consumer_key( $consumer_key ) { - - $user_query = new WP_User_Query( - array( - 'meta_key' => 'woocommerce_api_consumer_key', - 'meta_value' => $consumer_key, - ) - ); - - $users = $user_query->get_results(); - - if ( empty( $users[0] ) ) - throw new Exception( __( 'Consumer Key is invalid', 'woocommerce' ), 401 ); - - return $users[0]; - } - - /** - * Check if the consumer secret provided for the given user is valid - * - * @since 2.1 - * @param WP_User $user - * @param string $consumer_secret - * @return bool - */ - private function is_consumer_secret_valid( WP_User $user, $consumer_secret ) { - - return $user->woocommerce_api_consumer_secret === $consumer_secret; - } - - /** - * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer - * has a valid key/secret - * - * @param WP_User $user - * @param array $params the request parameters - * @throws Exception - */ - private function check_oauth_signature( $user, $params ) { - - $http_method = strtoupper( WC()->api->server->method ); - - $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path ); - - // get the signature provided by the consumer and remove it from the parameters prior to checking the signature - $consumer_signature = rawurldecode( $params['oauth_signature'] ); - unset( $params['oauth_signature'] ); - - // remove filters and convert them from array to strings to void normalize issues - if ( isset( $params['filter'] ) ) { - $filters = $params['filter']; - unset( $params['filter'] ); - foreach ( $filters as $filter => $filter_value ) { - $params['filter[' . $filter . ']'] = $filter_value; - } - } - - // normalize parameter key/values - $params = $this->normalize_parameters( $params ); - - // sort parameters - if ( ! uksort( $params, 'strcmp' ) ) { - throw new Exception( __( 'Invalid Signature - failed to sort parameters', 'woocommerce' ), 401 ); - } - - // form query string - $query_params = array(); - foreach ( $params as $param_key => $param_value ) { - - $query_params[] = $param_key . '%3D' . $param_value; // join with equals sign - } - $query_string = implode( '%26', $query_params ); // join with ampersand - - $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; - - if ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { - throw new Exception( __( 'Invalid Signature - signature method is invalid', 'woocommerce' ), 401 ); - } - - $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); - - $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $user->woocommerce_api_consumer_secret, true ) ); - - if ( $signature !== $consumer_signature ) { - throw new Exception( __( 'Invalid Signature - provided signature does not match', 'woocommerce' ), 401 ); - } - } - - /** - * Normalize each parameter by assuming each parameter may have already been - * encoded, so attempt to decode, and then re-encode according to RFC 3986 - * - * Note both the key and value is normalized so a filter param like: - * - * 'filter[period]' => 'week' - * - * is encoded to: - * - * 'filter%5Bperiod%5D' => 'week' - * - * This conforms to the OAuth 1.0a spec which indicates the entire query string - * should be URL encoded - * - * @since 2.1 - * @see rawurlencode() - * @param array $parameters un-normalized pararmeters - * @return array normalized parameters - */ - private function normalize_parameters( $parameters ) { - - $normalized_parameters = array(); - - foreach ( $parameters as $key => $value ) { - - // percent symbols (%) must be double-encoded - $key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) ); - $value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); - - $normalized_parameters[ $key ] = $value; - } - - return $normalized_parameters; - } - - /** - * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where - * an attacker could attempt to re-send an intercepted request at a later time. - * - * - A timestamp is valid if it is within 15 minutes of now - * - A nonce is valid if it has not been used within the last 15 minutes - * - * @param WP_User $user - * @param int $timestamp the unix timestamp for when the request was made - * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated - * @throws Exception - */ - private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) { - - $valid_window = 15 * 60; // 15 minute window - - if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) - throw new Exception( __( 'Invalid timestamp', 'woocommerce' ) ); - - $used_nonces = $user->woocommerce_api_nonces; - - if ( empty( $used_nonces ) ) - $used_nonces = array(); - - if ( in_array( $nonce, $used_nonces ) ) - throw new Exception( __( 'Invalid nonce - nonce has already been used', 'woocommerce' ), 401 ); - - $used_nonces[ $timestamp ] = $nonce; - - // remove expired nonces - foreach( $used_nonces as $nonce_timestamp => $nonce ) { - - if ( $nonce_timestamp < ( time() - $valid_window ) ) - unset( $used_nonces[ $nonce_timestamp ] ); - } - - update_user_meta( $user->ID, 'woocommerce_api_nonces', $used_nonces ); - } - - /** - * Check that the API keys provided have the proper key-specific permissions to either read or write API resources - * - * @param WP_User $user - * @throws Exception if the permission check fails - */ - public function check_api_key_permissions( $user ) { - - $key_permissions = $user->woocommerce_api_key_permissions; - - switch ( WC()->api->server->method ) { - - case 'HEAD': - case 'GET': - if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) { - throw new Exception( __( 'The API key provided does not have read permissions', 'woocommerce' ), 401 ); - } - break; - - case 'POST': - case 'PUT': - case 'PATCH': - case 'DELETE': - if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) { - throw new Exception( __( 'The API key provided does not have write permissions', 'woocommerce' ), 401 ); - } - break; - } - } -} diff --git a/includes/api/class-wc-api-coupons.php b/includes/api/class-wc-api-coupons.php deleted file mode 100644 index 0cd10922741..00000000000 --- a/includes/api/class-wc-api-coupons.php +++ /dev/null @@ -1,444 +0,0 @@ - - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET/POST /coupons - $routes[ $this->base ] = array( - array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ), - array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /coupons/count - $routes[ $this->base . '/count'] = array( - array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ), - ); - - # GET/PUT/DELETE /coupons/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_coupon' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), - array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ), - ); - - # GET /coupons/code/, note that coupon codes can contain spaces, dashes and underscores - $routes[ $this->base . '/code/(?P\w[\w\s\-]*)' ] = array( - array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ), - ); - - return $routes; - } - - /** - * Get all coupons - * - * @since 2.1 - * @param string $fields - * @param array $filter - * @param int $page - * @return array - */ - public function get_coupons( $fields = null, $filter = array(), $page = 1 ) { - - $filter['page'] = $page; - - $query = $this->query_coupons( $filter ); - - $coupons = array(); - - foreach( $query->posts as $coupon_id ) { - - if ( ! $this->is_readable( $coupon_id ) ) { - continue; - } - - $coupons[] = current( $this->get_coupon( $coupon_id, $fields ) ); - } - - $this->server->add_pagination_headers( $query ); - - return array( 'coupons' => $coupons ); - } - - /** - * Get the coupon for the given ID - * - * @since 2.1 - * @param int $id the coupon ID - * @param string $fields fields to include in response - * @return array|WP_Error - */ - public function get_coupon( $id, $fields = null ) { - global $wpdb; - - $id = $this->validate_request( $id, 'shop_coupon', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - // get the coupon code - $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $id ) ); - - if ( is_null( $code ) ) { - return new WP_Error( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $coupon = new WC_Coupon( $code ); - - $coupon_post = get_post( $coupon->id ); - - $coupon_data = array( - 'id' => $coupon->id, - 'code' => $coupon->code, - 'type' => $coupon->type, - 'created_at' => $this->server->format_datetime( $coupon_post->post_date_gmt ), - 'updated_at' => $this->server->format_datetime( $coupon_post->post_modified_gmt ), - 'amount' => wc_format_decimal( $coupon->amount, 2 ), - 'individual_use' => ( 'yes' === $coupon->individual_use ), - 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), - 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), - 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, - 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, - 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, - 'usage_count' => (int) $coupon->usage_count, - 'expiry_date' => $this->server->format_datetime( $coupon->expiry_date ), - 'apply_before_tax' => $coupon->apply_before_tax(), - 'enable_free_shipping' => $coupon->enable_free_shipping(), - 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), - 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), - 'exclude_sale_items' => $coupon->exclude_sale_items(), - 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), - 'customer_emails' => $coupon->customer_email, - ); - - return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) ); - } - - /** - * Get the total number of coupons - * - * @since 2.1 - * @param array $filter - * @return array - */ - public function get_coupons_count( $filter = array() ) { - - $query = $this->query_coupons( $filter ); - - if ( ! current_user_can( 'read_private_shop_coupons' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), array( 'status' => 401 ) ); - } - - return array( 'count' => (int) $query->found_posts ); - } - - /** - * Get the coupon for the given code - * - * @since 2.1 - * @param string $code the coupon code - * @param string $fields fields to include in response - * @return int|WP_Error - */ - public function get_coupon_by_code( $code, $fields = null ) { - global $wpdb; - - $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $code ) ); - - if ( is_null( $id ) ) { - return new WP_Error( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), array( 'status' => 404 ) ); - } - - return $this->get_coupon( $id, $fields ); - } - - /** - * Create a coupon - * - * @since 2.2 - * @param array $data - * @return array - */ - public function create_coupon( $data ) { - global $wpdb; - - // Check user permission - if ( ! current_user_can( 'publish_shop_coupons' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), array( 'status' => 401 ) ); - } - - // Check if coupon code is specified - if ( ! isset( $data['code'] ) ) { - return new WP_Error( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s' ), 'code' ), array( 'status' => 400 ) ); - } - - $coupon_code = apply_filters( 'woocommerce_coupon_code', $data['code'] ); - - // Check for duplicate coupon codes - $coupon_found = $wpdb->get_var( $wpdb->prepare( " - SELECT $wpdb->posts.ID - FROM $wpdb->posts - WHERE $wpdb->posts.post_type = 'shop_coupon' - AND $wpdb->posts.post_status = 'publish' - AND $wpdb->posts.post_title = '%s' - ", $coupon_code ) ); - - if ( $coupon_found ) { - return new WP_Error( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists' ), array( 'status' => 400 ) ); - } - - $defaults = array( - 'type' => 'fixed_cart', - 'amount' => 0, - 'individual_use' => 'no', - 'product_ids' => array(), - 'exclude_product_ids' => array(), - 'usage_limit' => '', - 'usage_limit_per_user' => '', - 'limit_usage_to_x_items' => '', - 'usage_count' => '', - 'expiry_date' => '', - 'apply_before_tax' => 'yes', - 'free_shipping' => 'no', - 'product_categories' => array(), - 'exclude_product_categories' => array(), - 'exclude_sale_items' => 'no', - 'minimum_amount' => '', - 'customer_email' => array(), - ); - - $coupon_data = wp_parse_args( $data, $defaults ); - - // Validate coupon types - if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { - return new WP_Error( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), array( 'status' => 400 ) ); - } - - $new_coupon = array( - 'post_title' => $coupon_code, - 'post_content' => '', - 'post_status' => 'publish', - 'post_author' => get_current_user_id(), - 'post_type' => 'shop_coupon' - ); - - $id = wp_insert_post( $new_coupon, $wp_error = false ); - - if ( is_wp_error( $id ) ) { - return new WP_Error( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), array( 'status' => 400 ) ); - } - - // Add POST Meta - update_post_meta( $id, 'discount_type', $coupon_data['type'] ); - update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) ); - update_post_meta( $id, 'individual_use', $coupon_data['individual_use'] ); - update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) ); - update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) ); - update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) ); - update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) ); - update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) ); - update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) ); - update_post_meta( $id, 'expiry_date', wc_clean( $coupon_data['expiry_date'] ) ); - update_post_meta( $id, 'apply_before_tax', wc_clean( $coupon_data['apply_before_tax'] ) ); - update_post_meta( $id, 'free_shipping', wc_clean( $coupon_data['free_shipping'] ) ); - update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_categories'] ) ) ); - update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_categories'] ) ) ); - update_post_meta( $id, 'exclude_sale_items', wc_clean( $coupon_data['exclude_sale_items'] ) ); - update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) ); - update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_email'] ) ) ); - - do_action( 'woocommerce_api_create_coupon', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_coupon( $id ); - } - - /** - * Edit a coupon - * - * @since 2.2 - * @param int $id the coupon ID - * @param array $data - * @return array - */ - public function edit_coupon( $id, $data ) { - - $id = $this->validate_request( $id, 'shop_coupon', 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - if ( isset( $data['code'] ) ) { - global $wpdb; - - $coupon_code = apply_filters( 'woocommerce_coupon_code', $data['code'] ); - - // Check for duplicate coupon codes - $coupon_found = $wpdb->get_var( $wpdb->prepare( " - SELECT $wpdb->posts.ID - FROM $wpdb->posts - WHERE $wpdb->posts.post_type = 'shop_coupon' - AND $wpdb->posts.post_status = 'publish' - AND $wpdb->posts.post_title = '%s' - AND $wpdb->posts.ID != %s - ", $coupon_code, $id ) ); - - if ( $coupon_found ) { - return new WP_Error( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists' ), array( 'status' => 400 ) ); - } - - $id = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) ); - if ( 0 === $id ) { - return new WP_Error( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce'), array( 'status' => 400 ) ); - } - } - - if ( isset( $data['type'] ) ) { - // Validate coupon types - if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { - return new WP_Error( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), array( 'status' => 400 ) ); - } - update_post_meta( $id, 'discount_type', $data['type'] ); - } - - if ( isset( $data['amount'] ) ) { - update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); - } - - if ( isset( $data['individual_use'] ) ) { - update_post_meta( $id, 'individual_use', $data['individual_use'] ); - } - - if ( isset( $data['product_ids'] ) ) { - update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); - } - - if ( isset( $data['exclude_product_ids'] ) ) { - update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); - } - - if ( isset( $data['usage_limit'] ) ) { - update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) ); - } - - if ( isset( $data['usage_limit_per_user'] ) ) { - update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); - } - - if ( isset( $data['limit_usage_to_x_items'] ) ) { - update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); - } - - if ( isset( $data['usage_count'] ) ) { - update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) ); - } - - if ( isset( $data['expiry_date'] ) ) { - update_post_meta( $id, 'expiry_date', wc_clean( $data['expiry_date'] ) ); - } - - if ( isset( $data['apply_before_tax'] ) ) { - update_post_meta( $id, 'apply_before_tax', wc_clean( $data['apply_before_tax'] ) ); - } - - if ( isset( $data['free_shipping'] ) ) { - update_post_meta( $id, 'free_shipping', wc_clean( $data['free_shipping'] ) ); - } - - if ( isset( $data['product_categories'] ) ) { - update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_categories'] ) ) ); - } - - if ( isset( $data['exclude_product_categories'] ) ) { - update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_categories'] ) ) ); - } - - if ( isset( $data['exclude_sale_items'] ) ) { - update_post_meta( $id, 'exclude_sale_items', wc_clean( $data['exclude_sale_items'] ) ); - } - - if ( isset( $data['minimum_amount'] ) ) { - update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); - } - - if ( isset( $data['customer_email'] ) ) { - update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_email'] ) ) ); - } - - do_action( 'woocommerce_api_edit_coupon', $id, $data ); - - return $this->get_coupon( $id ); - } - - /** - * Delete a coupon - * - * @since 2.2 - * @param int $id the coupon ID - * @param bool $force true to permanently delete coupon, false to move to trash - * @return array - */ - public function delete_coupon( $id, $force = false ) { - - $id = $this->validate_request( $id, 'shop_coupon', 'delete' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) ); - } - - /** - * Helper method to get coupon post objects - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_Query - */ - private function query_coupons( $args ) { - - // set base query arguments - $query_args = array( - 'fields' => 'ids', - 'post_type' => 'shop_coupon', - 'post_status' => 'publish', - ); - - $query_args = $this->merge_query_args( $query_args, $args ); - - return new WP_Query( $query_args ); - } - -} diff --git a/includes/api/class-wc-api-customers.php b/includes/api/class-wc-api-customers.php deleted file mode 100644 index b30583c5966..00000000000 --- a/includes/api/class-wc-api-customers.php +++ /dev/null @@ -1,711 +0,0 @@ - - * GET /customers//orders - * - * @since 2.2 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET/POST /customers - $routes[ $this->base ] = array( - array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ), - array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /customers/count - $routes[ $this->base . '/count'] = array( - array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ), - ); - - # GET/PUT/DELETE /customers/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), - array( array( $this, 'edit_customer' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), - array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ), - ); - - # GET /customers/ - $routes[ $this->base . '/email/(?P.+)' ] = array( - array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), - ); - - # GET /customers//orders - $routes[ $this->base . '/(?P\d+)/orders' ] = array( - array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), - ); - - # GET /customers//downloads - $routes[ $this->base . '/(?P\d+)/downloads' ] = array( - array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ), - ); - - return $routes; - } - - /** - * Get customer billing address fields. - * - * @since 2.2 - * @return array - */ - public function get_customer_billing_address() { - $billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array( - 'first_name', - 'last_name', - 'company', - 'address_1', - 'address_2', - 'city', - 'state', - 'postcode', - 'country', - 'email', - 'phone', - ) ); - - return $billing_address; - } - - /** - * Get customer shipping address fields. - * - * @since 2.2 - * @return array - */ - public function get_customer_shipping_address() { - $shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array( - 'first_name', - 'last_name', - 'company', - 'address_1', - 'address_2', - 'city', - 'state', - 'postcode', - 'country', - ) ); - - return $shipping_address; - } - - /** - * Get all customers - * - * @since 2.1 - * @param array $fields - * @param array $filter - * @param int $page - * @return array - */ - public function get_customers( $fields = null, $filter = array(), $page = 1 ) { - - $filter['page'] = $page; - - $query = $this->query_customers( $filter ); - - $customers = array(); - - foreach ( $query->get_results() as $user_id ) { - - if ( ! $this->is_readable( $user_id ) ) { - continue; - } - - $customers[] = current( $this->get_customer( $user_id, $fields ) ); - } - - $this->server->add_pagination_headers( $query ); - - return array( 'customers' => $customers ); - } - - /** - * Get the customer for the given ID - * - * @since 2.1 - * @param int $id the customer ID - * @param string $fields - * @return array - */ - public function get_customer( $id, $fields = null ) { - global $wpdb; - - $id = $this->validate_request( $id, 'customer', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $customer = new WP_User( $id ); - - // get info about user's last order - $last_order = $wpdb->get_row( "SELECT id, post_date_gmt - FROM $wpdb->posts AS posts - LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id - WHERE meta.meta_key = '_customer_user' - AND meta.meta_value = {$customer->ID} - AND posts.post_type = 'shop_order' - AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) - " ); - - $customer_data = array( - 'id' => $customer->ID, - 'created_at' => $this->server->format_datetime( $customer->user_registered ), - 'email' => $customer->user_email, - 'first_name' => $customer->first_name, - 'last_name' => $customer->last_name, - 'username' => $customer->user_login, - 'last_order_id' => is_object( $last_order ) ? $last_order->id : null, - 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->post_date_gmt ) : null, - 'orders_count' => (int) $customer->_order_count, - 'total_spent' => wc_format_decimal( $customer->_money_spent, 2 ), - 'avatar_url' => $this->get_avatar_url( $customer->customer_email ), - 'billing_address' => array( - 'first_name' => $customer->billing_first_name, - 'last_name' => $customer->billing_last_name, - 'company' => $customer->billing_company, - 'address_1' => $customer->billing_address_1, - 'address_2' => $customer->billing_address_2, - 'city' => $customer->billing_city, - 'state' => $customer->billing_state, - 'postcode' => $customer->billing_postcode, - 'country' => $customer->billing_country, - 'email' => $customer->billing_email, - 'phone' => $customer->billing_phone, - ), - 'shipping_address' => array( - 'first_name' => $customer->shipping_first_name, - 'last_name' => $customer->shipping_last_name, - 'company' => $customer->shipping_company, - 'address_1' => $customer->shipping_address_1, - 'address_2' => $customer->shipping_address_2, - 'city' => $customer->shipping_city, - 'state' => $customer->shipping_state, - 'postcode' => $customer->shipping_postcode, - 'country' => $customer->shipping_country, - ), - ); - - return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) ); - } - - /** - * Get the customer for the given email - * - * @since 2.1 - * @param string $email the customer email - * @param string $fields - * @return array - */ - function get_customer_by_email( $email, $fields = null ) { - - if ( is_email( $email ) ) { - $customer = get_user_by( 'email', $email ); - if ( ! is_object( $customer ) ) { - return new WP_Error( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer Email', 'woocommerce' ), array( 'status' => 404 ) ); - } - } else { - return new WP_Error( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer Email', 'woocommerce' ), array( 'status' => 404 ) ); - } - - return $this->get_customer( $customer->ID, $fields ); - } - - /** - * Get the total number of customers - * - * @since 2.1 - * @param array $filter - * @return array - */ - public function get_customers_count( $filter = array() ) { - - $query = $this->query_customers( $filter ); - - if ( ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), array( 'status' => 401 ) ); - } - - return array( 'count' => count( $query->get_results() ) ); - } - - /** - * Add/Update customer data. - * - * @since 2.2 - * @param int $id the customer ID - * @param array $data - * @return void - */ - protected function update_customer_data( $id, $data ) { - // Customer first name. - if ( isset( $data['first_name'] ) ) { - update_user_meta( $id, 'first_name', wc_clean( $data['first_name'] ) ); - } - - // Customer last name. - if ( isset( $data['last_name'] ) ) { - update_user_meta( $id, 'last_name', wc_clean( $data['last_name'] ) ); - } - - // Customer billing address. - if ( isset( $data['billing_address'] ) ) { - foreach ( $this->get_customer_billing_address() as $address ) { - if ( isset( $data['billing_address'][ $address ] ) ) { - update_user_meta( $id, 'billing_' . $address, wc_clean( $data['billing_address'][ $address ] ) ); - } - } - } - - // Customer shipping address. - if ( isset( $data['shipping_address'] ) ) { - foreach ( $this->get_customer_shipping_address() as $address ) { - if ( isset( $data['shipping_address'][ $address ] ) ) { - update_user_meta( $id, 'shipping_' . $address, wc_clean( $data['shipping_address'][ $address ] ) ); - } - } - } - - do_action( 'woocommerce_api_update_customer_data', $id, $data ); - } - - /** - * Create a customer - * - * @since 2.2 - * @param array $data - * @return array - */ - public function create_customer( $data ) { - - // Checks with can create new users. - if ( ! current_user_can( 'create_users' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) ); - } - - // Checks with the email is missing. - if ( ! isset( $data['email'] ) ) { - return new WP_Error( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), array( 'status' => 400 ) ); - } - - // Sets the username. - if ( ! isset( $data['username'] ) ) { - $data['username'] = ''; - } - - // Sets the password. - if ( ! isset( $data['password'] ) ) { - $data['password'] = wp_generate_password(); - } - - // Attempts to create the new customer - $id = wc_create_new_customer( $data['email'], $data['username'], $data['password'] ); - - // Checks for an error in the customer creation. - if ( is_wp_error( $id ) ) { - return new WP_Error( 'woocommerce_api_cannot_create_customer', $id->get_error_message(), array( 'status' => 400 ) ); - } - - // Added customer data. - $this->update_customer_data( $id, $data ); - - do_action( 'woocommerce_api_create_customer', $id, $data ); - - $this->server->send_status( 201 ); - - return $this->get_customer( $id ); - } - - /** - * Edit a customer - * - * @since 2.2 - * @param int $id the customer ID - * @param array $data - * @return array - */ - public function edit_customer( $id, $data ) { - - // Validate the customer ID. - $id = $this->validate_request( $id, 'customer', 'edit' ); - - // Return the validate error. - if ( is_wp_error( $id ) ) { - return $id; - } - - // Customer email. - if ( isset( $data['email'] ) ) { - wp_update_user( array( 'ID' => $id, 'user_email' => sanitize_email( $data['email'] ) ) ); - } - - // Customer password. - if ( isset( $data['password'] ) ) { - wp_update_user( array( 'ID' => $id, 'user_pass' => wc_clean( $data['password'] ) ) ); - } - - // Update customer data. - $this->update_customer_data( $id, $data ); - - do_action( 'woocommerce_api_edit_customer', $id, $data ); - - return $this->get_customer( $id ); - } - - /** - * Delete a customer - * - * @since 2.2 - * @param int $id the customer ID - * @return array - */ - public function delete_customer( $id ) { - - // Validate the customer ID. - $id = $this->validate_request( $id, 'customer', 'delete' ); - - // Return the validate error. - if ( is_wp_error( $id ) ) { - return $id; - } - - return $this->delete( $id, 'customer' ); - } - - /** - * Get the orders for a customer - * - * @since 2.1 - * @param int $id the customer ID - * @param string $fields fields to include in response - * @return array - */ - public function get_customer_orders( $id, $fields = null ) { - global $wpdb; - - $id = $this->validate_request( $id, 'customer', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $order_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id - FROM $wpdb->posts AS posts - LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id - WHERE meta.meta_key = '_customer_user' - AND meta.meta_value = '%s' - AND posts.post_type = 'shop_order' - AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) - ", $id ) ); - - if ( empty( $order_ids ) ) { - return array( 'orders' => array() ); - } - - $orders = array(); - - foreach ( $order_ids as $order_id ) { - $orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) ); - } - - return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) ); - } - - /** - * Get the available downloads for a customer - * - * @since 2.2 - * @param int $id the customer ID - * @param string $fields fields to include in response - * @return array - */ - public function get_customer_downloads( $id, $fields = null ) { - $id = $this->validate_request( $id, 'customer', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $downloads = wc_get_customer_available_downloads( $id ); - - if ( empty( $downloads ) ) { - return array( 'downloads' => array() ); - } - - return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) ); - } - - /** - * Helper method to get customer user objects - * - * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited - * pagination support - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_User_Query - */ - private function query_customers( $args = array() ) { - - // default users per page - $users_per_page = get_option( 'posts_per_page' ); - - // set base query arguments - $query_args = array( - 'fields' => 'ID', - 'role' => 'customer', - 'orderby' => 'registered', - 'number' => $users_per_page, - ); - - // search - if ( ! empty( $args['q'] ) ) { - $query_args['search'] = $args['q']; - } - - // limit number of users returned - if ( ! empty( $args['limit'] ) ) { - - $query_args['number'] = absint( $args['limit'] ); - - $users_per_page = absint( $args['limit'] ); - } - - // page - $page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1; - - // offset - if ( ! empty( $args['offset'] ) ) { - $query_args['offset'] = absint( $args['offset'] ); - } else { - $query_args['offset'] = $users_per_page * ( $page - 1 ); - } - - // created date - if ( ! empty( $args['created_at_min'] ) ) { - $this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] ); - } - - if ( ! empty( $args['created_at_max'] ) ) { - $this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] ); - } - - $query = new WP_User_Query( $query_args ); - - // helper members for pagination headers - $query->total_pages = ceil( $query->get_total() / $users_per_page ); - $query->page = $page; - - return $query; - } - - /** - * Add customer data to orders - * - * @since 2.1 - * @param $order_data - * @param $order - * @return array - */ - public function add_customer_data( $order_data, $order ) { - - if ( 0 == $order->customer_user ) { - - // add customer data from order - $order_data['customer'] = array( - 'id' => 0, - 'email' => $order->billing_email, - 'first_name' => $order->billing_first_name, - 'last_name' => $order->billing_last_name, - 'billing_address' => array( - 'first_name' => $order->billing_first_name, - 'last_name' => $order->billing_last_name, - 'company' => $order->billing_company, - 'address_1' => $order->billing_address_1, - 'address_2' => $order->billing_address_2, - 'city' => $order->billing_city, - 'state' => $order->billing_state, - 'postcode' => $order->billing_postcode, - 'country' => $order->billing_country, - 'email' => $order->billing_email, - 'phone' => $order->billing_phone, - ), - 'shipping_address' => array( - 'first_name' => $order->shipping_first_name, - 'last_name' => $order->shipping_last_name, - 'company' => $order->shipping_company, - 'address_1' => $order->shipping_address_1, - 'address_2' => $order->shipping_address_2, - 'city' => $order->shipping_city, - 'state' => $order->shipping_state, - 'postcode' => $order->shipping_postcode, - 'country' => $order->shipping_country, - ), - ); - - } else { - - $order_data['customer'] = current( $this->get_customer( $order->customer_user ) ); - } - - return $order_data; - } - - /** - * Modify the WP_User_Query to support filtering on the date the customer was created - * - * @since 2.1 - * @param WP_User_Query $query - */ - public function modify_user_query( $query ) { - - if ( $this->created_at_min ) { - $query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) ); - } - - if ( $this->created_at_max ) { - $query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) ); - } - } - - /** - * Wrapper for @see get_avatar() which doesn't simply return - * the URL so we need to pluck it from the HTML img tag - * - * Kudos to https://github.com/WP-API/WP-API for offering a better solution - * - * @since 2.1 - * @param string $email the customer's email - * @return string the URL to the customer's avatar - */ - private function get_avatar_url( $email ) { - $avatar_html = get_avatar( $email ); - - // Get the URL of the avatar from the provided HTML - preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches ); - - if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { - return esc_url_raw( $matches[1] ); - } - - return null; - } - - /** - * Validate the request by checking: - * - * 1) the ID is a valid integer - * 2) the ID returns a valid WP_User - * 3) the current user has the proper permissions - * - * @since 2.1 - * @see WC_API_Resource::validate_request() - * @param string|int $id the customer ID - * @param string $type the request type, unused because this method overrides the parent class - * @param string $context the context of the request, either `read`, `edit` or `delete` - * @return int|WP_Error valid user ID or WP_Error if any of the checks fails - */ - protected function validate_request( $id, $type, $context ) { - - $id = absint( $id ); - - // validate ID - if ( empty( $id ) ) { - return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) ); - } - - // non-existent IDs return a valid WP_User object with the user ID = 0 - $customer = new WP_User( $id ); - - if ( 0 === $customer->ID ) { - return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) ); - } - - // validate permissions - switch ( $context ) { - - case 'read': - if ( ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) ); - } - break; - - case 'edit': - if ( ! current_user_can( 'edit_users' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) ); - } - break; - - case 'delete': - if ( ! current_user_can( 'delete_users' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) ); - } - break; - } - - return $id; - } - - /** - * Check if the current user can read users - * - * @since 2.1 - * @see WC_API_Resource::is_readable() - * @param int|WP_Post $post unused - * @return bool true if the current user can read users, false otherwise - */ - protected function is_readable( $post ) { - - return current_user_can( 'list_users' ); - } - -} diff --git a/includes/api/class-wc-api-json-handler.php b/includes/api/class-wc-api-json-handler.php deleted file mode 100644 index 57d251598a3..00000000000 --- a/includes/api/class-wc-api-json-handler.php +++ /dev/null @@ -1,73 +0,0 @@ -api->server->send_status( 400 ); - - $data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ); - } - - // Check for invalid characters (only alphanumeric allowed) - if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) { - - WC()->api->server->send_status( 400 ); - - $data = array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ); - } - - return $_GET['_jsonp'] . '(' . json_encode( $data ) . ')'; - } - - return json_encode( $data ); - } - -} diff --git a/includes/api/class-wc-api-orders.php b/includes/api/class-wc-api-orders.php deleted file mode 100644 index d7860538a62..00000000000 --- a/includes/api/class-wc-api-orders.php +++ /dev/null @@ -1,396 +0,0 @@ - - * GET /orders//notes - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET /orders - $routes[ $this->base ] = array( - array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), - ); - - # GET /orders/count - $routes[ $this->base . '/count'] = array( - array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), - ); - - # GET|PUT /orders/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_order' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /orders//notes - $routes[ $this->base . '/(?P\d+)/notes' ] = array( - array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), - ); - - return $routes; - } - - /** - * Get all orders - * - * @since 2.1 - * @param string $fields - * @param array $filter - * @param string $status - * @param int $page - * @return array - */ - public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { - - if ( ! empty( $status ) ) - $filter['status'] = $status; - - $filter['page'] = $page; - - $query = $this->query_orders( $filter ); - - $orders = array(); - - foreach( $query->posts as $order_id ) { - - if ( ! $this->is_readable( $order_id ) ) - continue; - - $orders[] = current( $this->get_order( $order_id, $fields ) ); - } - - $this->server->add_pagination_headers( $query ); - - return array( 'orders' => $orders ); - } - - - /** - * Get the order for the given ID - * - * @since 2.1 - * @param int $id the order ID - * @param array $fields - * @return array - */ - public function get_order( $id, $fields = null ) { - - // ensure order ID is valid & user has permission to read - $id = $this->validate_request( $id, 'shop_order', 'read' ); - - if ( is_wp_error( $id ) ) - return $id; - - $order = get_order( $id ); - - $order_post = get_post( $id ); - - $order_data = array( - 'id' => $order->id, - 'order_number' => $order->get_order_number(), - 'created_at' => $this->server->format_datetime( $order_post->post_date_gmt ), - 'updated_at' => $this->server->format_datetime( $order_post->post_modified_gmt ), - 'completed_at' => $this->server->format_datetime( $order->completed_date, true ), - 'status' => $order->get_status(), - 'currency' => $order->order_currency, - 'total' => wc_format_decimal( $order->get_total(), 2 ), - 'subtotal' => wc_format_decimal( $this->get_order_subtotal( $order ), 2 ), - 'total_line_items_quantity' => $order->get_item_count(), - 'total_tax' => wc_format_decimal( $order->get_total_tax(), 2 ), - 'total_shipping' => wc_format_decimal( $order->get_total_shipping(), 2 ), - 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), 2 ), - 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), 2 ), - 'total_discount' => wc_format_decimal( $order->get_total_discount(), 2 ), - 'cart_discount' => wc_format_decimal( $order->get_cart_discount(), 2 ), - 'order_discount' => wc_format_decimal( $order->get_order_discount(), 2 ), - 'shipping_methods' => $order->get_shipping_method(), - 'payment_details' => array( - 'method_id' => $order->payment_method, - 'method_title' => $order->payment_method_title, - 'paid' => isset( $order->paid_date ), - ), - 'billing_address' => array( - 'first_name' => $order->billing_first_name, - 'last_name' => $order->billing_last_name, - 'company' => $order->billing_company, - 'address_1' => $order->billing_address_1, - 'address_2' => $order->billing_address_2, - 'city' => $order->billing_city, - 'state' => $order->billing_state, - 'postcode' => $order->billing_postcode, - 'country' => $order->billing_country, - 'email' => $order->billing_email, - 'phone' => $order->billing_phone, - ), - 'shipping_address' => array( - 'first_name' => $order->shipping_first_name, - 'last_name' => $order->shipping_last_name, - 'company' => $order->shipping_company, - 'address_1' => $order->shipping_address_1, - 'address_2' => $order->shipping_address_2, - 'city' => $order->shipping_city, - 'state' => $order->shipping_state, - 'postcode' => $order->shipping_postcode, - 'country' => $order->shipping_country, - ), - 'note' => $order->customer_note, - 'customer_ip' => $order->customer_ip_address, - 'customer_user_agent' => $order->customer_user_agent, - 'customer_id' => $order->customer_user, - 'view_order_url' => $order->get_view_order_url(), - 'line_items' => array(), - 'shipping_lines' => array(), - 'tax_lines' => array(), - 'fee_lines' => array(), - 'coupon_lines' => array(), - ); - - // add line items - foreach( $order->get_items() as $item_id => $item ) { - - $product = $order->get_product_from_item( $item ); - - $order_data['line_items'][] = array( - 'id' => $item_id, - 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), - 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], 2 ), - 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), - 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), - 'quantity' => (int) $item['qty'], - 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null, - 'name' => $item['name'], - 'product_id' => ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id, - 'sku' => is_object( $product ) ? $product->get_sku() : null, - ); - } - - // add shipping - foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { - - $order_data['shipping_lines'][] = array( - 'id' => $shipping_item_id, - 'method_id' => $shipping_item['method_id'], - 'method_title' => $shipping_item['name'], - 'total' => wc_format_decimal( $shipping_item['cost'], 2 ), - ); - } - - // add taxes - foreach ( $order->get_tax_totals() as $tax_code => $tax ) { - - $order_data['tax_lines'][] = array( - 'code' => $tax_code, - 'title' => $tax->label, - 'total' => wc_format_decimal( $tax->amount, 2 ), - 'compound' => (bool) $tax->is_compound, - ); - } - - // add fees - foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { - - $order_data['fee_lines'][] = array( - 'id' => $fee_item_id, - 'title' => $fee_item['name'], - 'tax_class' => ( ! empty( $fee_item['tax_class'] ) ) ? $fee_item['tax_class'] : null, - 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), 2 ), - 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), 2 ), - ); - } - - // add coupons - foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { - - $order_data['coupon_lines'][] = array( - 'id' => $coupon_item_id, - 'code' => $coupon_item['name'], - 'amount' => wc_format_decimal( $coupon_item['discount_amount'], 2 ), - ); - } - - return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); - } - - /** - * Get the total number of orders - * - * @since 2.1 - * @param string $status - * @param array $filter - * @return array - */ - public function get_orders_count( $status = null, $filter = array() ) { - - if ( ! empty( $status ) ) - $filter['status'] = $status; - - $query = $this->query_orders( $filter ); - - if ( ! current_user_can( 'read_private_shop_orders' ) ) - return new WP_Error( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), array( 'status' => 401 ) ); - - return array( 'count' => (int) $query->found_posts ); - } - - /** - * Edit an order - * - * API v1 only allows updating the status of an order - * - * @since 2.1 - * @param int $id the order ID - * @param array $data - * @return array - */ - public function edit_order( $id, $data ) { - - $id = $this->validate_request( $id, 'shop_order', 'edit' ); - - if ( is_wp_error( $id ) ) - return $id; - - $order = get_order( $id ); - - if ( ! empty( $data['status'] ) ) { - - $order->update_status( $data['status'], isset( $data['note'] ) ? $data['note'] : '' ); - } - - return $this->get_order( $id ); - } - - /** - * Delete an order - * - * @TODO enable along with POST in 2.2 - * @param int $id the order ID - * @param bool $force true to permanently delete order, false to move to trash - * @return array - */ - public function delete_order( $id, $force = false ) { - - $id = $this->validate_request( $id, 'shop_order', 'delete' ); - - return $this->delete( $id, 'order', ( 'true' === $force ) ); - } - - /** - * Get the admin order notes for an order - * - * @since 2.1 - * @param int $id the order ID - * @param string $fields fields to include in response - * @return array - */ - public function get_order_notes( $id, $fields = null ) { - - // ensure ID is valid order ID - $id = $this->validate_request( $id, 'shop_order', 'read' ); - - if ( is_wp_error( $id ) ) - return $id; - - $args = array( - 'post_id' => $id, - 'approve' => 'approve', - 'type' => 'order_note' - ); - - remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); - - $notes = get_comments( $args ); - - add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); - - $order_notes = array(); - - foreach ( $notes as $note ) { - - $order_notes[] = array( - 'id' => $note->comment_ID, - 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), - 'note' => $note->comment_content, - 'customer_note' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false, - ); - } - - return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $id, $fields, $notes, $this->server ) ); - } - - /** - * Helper method to get order post objects - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_Query - */ - private function query_orders( $args ) { - - // set base query arguments - $query_args = array( - 'fields' => 'ids', - 'post_type' => 'shop_order' - ); - - // add status argument - if ( ! empty( $args['status'] ) ) { - $statuses = explode( ',', $args['status'] ); - $query_args['post_status'] = $statuses; - - unset( $args['status'] ); - } else { - $query_args['post_status'] = array_keys( wc_get_order_statuses() ); - } - - $query_args = $this->merge_query_args( $query_args, $args ); - - return new WP_Query( $query_args ); - } - - /** - * Helper method to get the order subtotal - * - * @since 2.1 - * @param WC_Order $order - * @return float - */ - private function get_order_subtotal( $order ) { - - $subtotal = 0; - - // subtotal - foreach ( $order->get_items() as $item ) { - - $subtotal += ( isset( $item['line_subtotal'] ) ) ? $item['line_subtotal'] : 0; - } - - return $subtotal; - } - -} diff --git a/includes/api/class-wc-api-products.php b/includes/api/class-wc-api-products.php deleted file mode 100644 index f1f3285335c..00000000000 --- a/includes/api/class-wc-api-products.php +++ /dev/null @@ -1,1676 +0,0 @@ - - * GET /products//reviews - * - * @since 2.1 - * @param array $routes - * @return array - */ - public function register_routes( $routes ) { - - # GET/POST /products - $routes[ $this->base ] = array( - array( array( $this, 'get_products' ), WC_API_Server::READABLE ), - array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), - ); - - # GET /products/count - $routes[ $this->base . '/count'] = array( - array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), - ); - - # GET/PUT/DELETE /products/ - $routes[ $this->base . '/(?P\d+)' ] = array( - array( array( $this, 'get_product' ), WC_API_Server::READABLE ), - array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), - array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), - ); - - # GET /products//reviews - $routes[ $this->base . '/(?P\d+)/reviews' ] = array( - array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), - ); - - return $routes; - } - - /** - * Get all products - * - * @since 2.1 - * @param string $fields - * @param string $type - * @param array $filter - * @param int $page - * @return array - */ - public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { - - if ( ! empty( $type ) ) { - $filter['type'] = $type; - } - - $filter['page'] = $page; - - $query = $this->query_products( $filter ); - - $products = array(); - - foreach ( $query->posts as $product_id ) { - - if ( ! $this->is_readable( $product_id ) ) { - continue; - } - - $products[] = current( $this->get_product( $product_id, $fields ) ); - } - - $this->server->add_pagination_headers( $query ); - - return array( 'products' => $products ); - } - - /** - * Get the product for the given ID - * - * @since 2.1 - * @param int $id the product ID - * @param string $fields - * @return array - */ - public function get_product( $id, $fields = null ) { - - $id = $this->validate_request( $id, 'product', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $product = get_product( $id ); - - // add data that applies to every product type - $product_data = $this->get_product_data( $product ); - - // add variations to variable products - if ( $product->is_type( 'variable' ) && $product->has_child() ) { - - $product_data['variations'] = $this->get_variation_data( $product ); - } - - // add the parent product data to an individual variation - if ( $product->is_type( 'variation' ) ) { - - $product_data['parent'] = $this->get_product_data( $product->parent ); - } - - return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); - } - - /** - * Get the total number of orders - * - * @since 2.1 - * @param string $type - * @param array $filter - * @return array - */ - public function get_products_count( $type = null, $filter = array() ) { - - if ( ! empty( $type ) ) { - $filter['type'] = $type; - } - - if ( ! current_user_can( 'read_private_products' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), array( 'status' => 401 ) ); - } - - $query = $this->query_products( $filter ); - - return array( 'count' => (int) $query->found_posts ); - } - - /** - * Create a new product - * - * @since 2.2 - * @param array $data posted data - * @return array - */ - public function create_product( $data ) { - - // Check permisions - if ( ! current_user_can( 'publish_products' ) ) { - return new WP_Error( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), array( 'status' => 401 ) ); - } - - // Check if product title is specified - if ( ! isset( $data['title'] ) ) { - return new WP_Error( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s' ), 'title' ), array( 'status' => 400 ) ); - } - - // Check product type - if ( ! isset( $data['type'] ) ) { - $data['type'] = 'simple'; - } - - // Validate the product type - if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { - return new WP_Error( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), array( 'status' => 400 ) ); - } - - $new_product = array( - 'post_title' => wc_clean( $data['title'] ), - 'post_status' => ( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ), - 'post_type' => 'product', - 'post_excerpt' => ( isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : '' ), - 'post_content' => ( isset( $data['description'] ) ? wc_clean( $data['description'] ) : '' ), - 'post_author' => get_current_user_id(), - ); - - // Attempts to create the new product - $id = wp_insert_post( $new_product, true ); - - // Checks for an error in the product creation - if ( is_wp_error( $id ) ) { - return new WP_Error( 'woocommerce_api_cannot_create_product', $id->get_error_message(), array( 'status' => 400 ) ); - } - - // Check for featured/gallery images, upload it and set it - if ( isset( $data['images'] ) ) { - $images = $this->save_product_images( $id, $data['images'] ); - - if ( is_wp_error( $images ) ) { - return $images; - } - } - - // Save product meta fields - $meta = $this->save_product_meta( $id, $data ); - if ( is_wp_error( $meta ) ) { - return $meta; - } - - // Save variations - if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { - $variations = $this->save_variations( $id, $data ); - - if ( is_wp_error( $variations ) ) { - return $variations; - } - } - - do_action( 'woocommerce_api_create_product', $id, $data ); - - // Clear cache/transients - wc_delete_product_transients( $id ); - - $this->server->send_status( 201 ); - - return $this->get_product( $id ); - } - - /** - * Edit a product - * - * @since 2.2 - * @param int $id the product ID - * @param array $data - * @return array - */ - public function edit_product( $id, $data ) { - - $id = $this->validate_request( $id, 'product', 'edit' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - // Product name. - if ( isset( $data['title'] ) ) { - wp_update_post( array( 'ID' => $id, 'post_title' => wc_clean( $data['title'] ) ) ); - } - - // Product status. - if ( isset( $data['status'] ) ) { - wp_update_post( array( 'ID' => $id, 'post_status' => wc_clean( $data['status'] ) ) ); - } - - // Product short description. - if ( isset( $data['short_description'] ) ) { - wp_update_post( array( 'ID' => $id, 'post_excerpt' => wc_clean( $data['short_description'] ) ) ); - } - - // Product description. - if ( isset( $data['description'] ) ) { - wp_update_post( array( 'ID' => $id, 'post_content' => wc_clean( $data['description'] ) ) ); - } - - // Validate the product type - if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { - return new WP_Error( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), array( 'status' => 400 ) ); - } - - // Check for featured/gallery images, upload it and set it - if ( isset( $data['images'] ) ) { - $images = $this->save_product_images( $id, $data['images'] ); - - if ( is_wp_error( $images ) ) { - return $images; - } - } - - // Save product meta fields - $meta = $this->save_product_meta( $id, $data ); - if ( is_wp_error( $meta ) ) { - return $meta; - } - - // Save variations - if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { - $variations = $this->save_variations( $id, $data ); - - if ( is_wp_error( $variations ) ) { - return $variations; - } - } - - do_action( 'woocommerce_api_edit_product', $id, $data ); - - // Clear cache/transients - wc_delete_product_transients( $id ); - - return $this->get_product( $id ); - } - - /** - * Delete a product - * - * @since 2.2 - * @param int $id the product ID - * @param bool $force true to permanently delete order, false to move to trash - * @return array - */ - public function delete_product( $id, $force = false ) { - - $id = $this->validate_request( $id, 'product', 'delete' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - return $this->delete( $id, 'product', ( 'true' === $force ) ); - } - - /** - * Get the reviews for a product - * - * @since 2.1 - * @param int $id the product ID to get reviews for - * @param string $fields fields to include in response - * @return array - */ - public function get_product_reviews( $id, $fields = null ) { - - $id = $this->validate_request( $id, 'product', 'read' ); - - if ( is_wp_error( $id ) ) { - return $id; - } - - $args = array( - 'post_id' => $id, - 'approve' => 'approve', - ); - - $comments = get_comments( $args ); - - $reviews = array(); - - foreach ( $comments as $comment ) { - - $reviews[] = array( - 'id' => $comment->comment_ID, - 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), - 'review' => $comment->comment_content, - 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), - 'reviewer_name' => $comment->comment_author, - 'reviewer_email' => $comment->comment_author_email, - 'verified' => (bool) wc_customer_bought_product( $comment->comment_author_email, $comment->user_id, $id ), - ); - } - - return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); - } - - /** - * Helper method to get product post objects - * - * @since 2.1 - * @param array $args request arguments for filtering query - * @return WP_Query - */ - private function query_products( $args ) { - - // set base query arguments - $query_args = array( - 'fields' => 'ids', - 'post_type' => 'product', - 'post_status' => 'publish', - 'meta_query' => array(), - ); - - if ( ! empty( $args['type'] ) ) { - - $types = explode( ',', $args['type'] ); - - $query_args['tax_query'] = array( - array( - 'taxonomy' => 'product_type', - 'field' => 'slug', - 'terms' => $types, - ), - ); - - unset( $args['type'] ); - } - - $query_args = $this->merge_query_args( $query_args, $args ); - - return new WP_Query( $query_args ); - } - - /** - * Get standard product data that applies to every product type - * - * @since 2.1 - * @param WC_Product $product - * @return array - */ - private function get_product_data( $product ) { - - return array( - 'title' => $product->get_title(), - 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, - 'created_at' => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ), - 'updated_at' => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ), - 'type' => $product->product_type, - 'status' => $product->get_post_data()->post_status, - 'downloadable' => $product->is_downloadable(), - 'virtual' => $product->is_virtual(), - 'permalink' => $product->get_permalink(), - 'sku' => $product->get_sku(), - 'price' => wc_format_decimal( $product->get_price(), 2 ), - 'regular_price' => wc_format_decimal( $product->get_regular_price(), 2 ), - 'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), 2 ) : null, - 'price_html' => $product->get_price_html(), - 'taxable' => $product->is_taxable(), - 'tax_status' => $product->get_tax_status(), - 'tax_class' => $product->get_tax_class(), - 'managing_stock' => $product->managing_stock(), - 'stock_quantity' => (int) $product->get_stock_quantity(), - 'in_stock' => $product->is_in_stock(), - 'backorders_allowed' => $product->backorders_allowed(), - 'backordered' => $product->is_on_backorder(), - 'sold_individually' => $product->is_sold_individually(), - 'purchaseable' => $product->is_purchasable(), - 'featured' => $product->is_featured(), - 'visible' => $product->is_visible(), - 'catalog_visibility' => $product->visibility, - 'on_sale' => $product->is_on_sale(), - 'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null, - 'dimensions' => array( - 'length' => $product->length, - 'width' => $product->width, - 'height' => $product->height, - 'unit' => get_option( 'woocommerce_dimension_unit' ), - ), - 'shipping_required' => $product->needs_shipping(), - 'shipping_taxable' => $product->is_shipping_taxable(), - 'shipping_class' => $product->get_shipping_class(), - 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, - 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ), - 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), - 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), - 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), - 'rating_count' => (int) $product->get_rating_count(), - 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ), - 'upsell_ids' => array_map( 'absint', $product->get_upsells() ), - 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), - 'categories' => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ), - 'tags' => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ), - 'images' => $this->get_images( $product ), - 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->is_type( 'variation' ) ? $product->variation_id : $product->id ) ), - 'attributes' => $this->get_attributes( $product ), - 'downloads' => $this->get_downloads( $product ), - 'download_limit' => (int) $product->download_limit, - 'download_expiry' => (int) $product->download_expiry, - 'download_type' => $product->download_type, - 'purchase_note' => wpautop( do_shortcode( $product->purchase_note ) ), - 'total_sales' => metadata_exists( 'post', $product->id, 'total_sales' ) ? (int) get_post_meta( $product->id, 'total_sales', true ) : 0, - 'variations' => array(), - 'parent' => array(), - ); - } - - /** - * Get an individual variation's data - * - * @since 2.1 - * @param WC_Product $product - * @return array - */ - private function get_variation_data( $product ) { - - $variations = array(); - - foreach ( $product->get_children() as $child_id ) { - - $variation = $product->get_child( $child_id ); - - if ( ! $variation->exists() ) { - continue; - } - - $variations[] = array( - 'id' => $variation->get_variation_id(), - 'created_at' => $this->server->format_datetime( $variation->get_post_data()->post_date_gmt ), - 'updated_at' => $this->server->format_datetime( $variation->get_post_data()->post_modified_gmt ), - 'downloadable' => $variation->is_downloadable(), - 'virtual' => $variation->is_virtual(), - 'permalink' => $variation->get_permalink(), - 'sku' => $variation->get_sku(), - 'price' => wc_format_decimal( $variation->get_price(), 2 ), - 'regular_price' => wc_format_decimal( $variation->get_regular_price(), 2 ), - 'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), 2 ) : null, - 'taxable' => $variation->is_taxable(), - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'stock_quantity' => (int) $variation->get_stock_quantity(), - 'in_stock' => $variation->is_in_stock(), - 'backordered' => $variation->is_on_backorder(), - 'purchaseable' => $variation->is_purchasable(), - 'visible' => $variation->variation_is_visible(), - 'on_sale' => $variation->is_on_sale(), - 'weight' => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null, - 'dimensions' => array( - 'length' => $variation->length, - 'width' => $variation->width, - 'height' => $variation->height, - 'unit' => get_option( 'woocommerce_dimension_unit' ), - ), - 'shipping_class' => $variation->get_shipping_class(), - 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, - 'image' => $this->get_images( $variation ), - 'attributes' => $this->get_attributes( $variation ), - 'downloads' => $this->get_downloads( $variation ), - 'download_limit' => (int) $product->download_limit, - 'download_expiry' => (int) $product->download_expiry, - ); - } - - return $variations; - } - - /** - * Save product meta - * - * @since 2.2 - * @param int $id - * @param array $data - * @return bool|WP_Error - */ - protected function save_product_meta( $id, $data ) { - // Product Type - $product_type = null; - if ( isset( $data['type'] ) ) { - $product_type = wc_clean( $data['type'] ); - wp_set_object_terms( $id, $product_type, 'product_type' ); - } else { - $_product_type = get_the_terms( $id, 'product_type' ); - if ( is_array( $_product_type ) ) { - $_product_type = current( $_product_type ); - $product_type = $_product_type->slug; - } - } - - // Virtual - if ( isset( $data['virtual'] ) ) { - update_post_meta( $id, '_virtual', ( true === $data['virtual'] ) ? 'yes' : 'no' ); - } - - // Tax status - if ( isset( $data['tax_status'] ) ) { - update_post_meta( $id, '_tax_status', wc_clean( $data['tax_status'] ) ); - } - - // Tax Class - if ( isset( $data['tax_class'] ) ) { - update_post_meta( $id, '_tax_class', wc_clean( $data['tax_class'] ) ); - } - - // Catalog Visibility - if ( isset( $data['catalog_visibility'] ) ) { - update_post_meta( $id, '_visibility', wc_clean( $data['catalog_visibility'] ) ); - } - - // Purchase Note - if ( isset( $data['purchase_note'] ) ) { - update_post_meta( $id, '_purchase_note', wc_clean( $data['purchase_note'] ) ); - } - - // Featured Product - if ( isset( $data['featured'] ) ) { - update_post_meta( $id, '_featured', ( true === $data['featured'] ) ? 'yes' : 'no' ); - } - - // Shipping data - $this->save_product_shipping_data( $id, $data ); - - // SKU - if ( isset( $data['sku'] ) ) { - $sku = get_post_meta( $id, '_sku', true ); - $new_sku = wc_clean( $data['sku'] ); - - if ( '' == $new_sku ) { - update_post_meta( $id, '_sku', '' ); - } elseif ( $new_sku !== $sku ) { - if ( ! empty( $new_sku ) ) { - $unique_sku = wc_product_has_unique_sku( $id, $new_sku ); - if ( ! $unique_sku ) { - return new WP_Error( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product' ), array( 'status' => 400 ) ); - } else { - update_post_meta( $id, '_sku', $new_sku ); - } - } else { - update_post_meta( $id, '_sku', '' ); - } - } - } - - // Attributes - if ( isset( $data['attributes'] ) ) { - $attributes = array(); - - foreach ( $data['attributes'] as $attribute ) { - $is_taxonomy = 0; - - if ( ! isset( $attribute['name'] ) ) { - continue; - } - - $taxonomy = $this->get_attribute_taxonomy_by_label( $attribute['name'] ); - if ( $taxonomy ) { - $is_taxonomy = 1; - } - - if ( $is_taxonomy ) { - - if ( isset( $attribute['options'] ) ) { - // Select based attributes - Format values (posted values are slugs) - if ( is_array( $attribute['options'] ) ) { - $values = array_map( 'sanitize_title', $attribute['options'] ); - - // Text based attributes - Posted values are term names - don't change to slugs - } else { - $values = array_map( 'stripslashes', array_map( 'strip_tags', explode( WC_DELIMITER, $attribute['options'] ) ) ); - } - - $values = array_filter( $values, 'strlen' ); - } else { - $values = array(); - } - - // Update post terms - if ( taxonomy_exists( $taxonomy ) ) { - wp_set_object_terms( $id, $values, $taxonomy ); - } - - if ( $values ) { - // Add attribute to array, but don't set values - $attributes[ $taxonomy ] = array( - 'name' => $taxonomy, - 'value' => '', - 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, - 'is_visible' => ( isset( $attribute['position'] ) && $attribute['position'] ) ? 1 : 0, - 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, - 'is_taxonomy' => $is_taxonomy - ); - } - - } elseif ( isset( $attribute['options'] ) ) { - // Array based - if ( is_array( $attribute['options'] ) ) { - $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); - - // Text based, separate by pipe - } else { - $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); - } - - // Custom attribute - Add attribute to array and set the values - $attributes[ sanitize_title( $attribute['name'] ) ] = array( - 'name' => wc_clean( $attribute['name'] ), - 'value' => $values, - 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, - 'is_visible' => ( isset( $attribute['position'] ) && $attribute['position'] ) ? 1 : 0, - 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, - 'is_taxonomy' => $is_taxonomy - ); - } - } - - if ( ! function_exists( 'attributes_cmp' ) ) { - function attributes_cmp( $a, $b ) { - if ( $a['position'] == $b['position'] ) { - return 0; - } - - return ( $a['position'] < $b['position'] ) ? -1 : 1; - } - } - uasort( $attributes, 'attributes_cmp' ); - - update_post_meta( $id, '_product_attributes', $attributes ); - } - - // Sales and prices - if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { - - // Variable and grouped products have no prices - update_post_meta( $id, '_regular_price', '' ); - update_post_meta( $id, '_sale_price', '' ); - update_post_meta( $id, '_sale_price_dates_from', '' ); - update_post_meta( $id, '_sale_price_dates_to', '' ); - update_post_meta( $id, '_price', '' ); - - } else { - - // Regular Price - if ( isset( $data['regular_price'] ) ) { - $regular_price = ( '' === $data['regular_price'] ) ? '' : wc_format_decimal( $data['regular_price'] ); - update_post_meta( $id, '_regular_price', $regular_price ); - } else { - $regular_price = get_post_meta( $id, '_regular_price', true ); - } - - // Sale Price - if ( isset( $data['sale_price'] ) ) { - $sale_price = ( '' === $data['sale_price'] ) ? '' : wc_format_decimal( $data['sale_price'] ); - update_post_meta( $id, '_sale_price', $sale_price ); - } else { - $sale_price = get_post_meta( $id, '_sale_price', true ); - } - - $date_from = isset( $data['sale_price_dates_from'] ) ? $data['sale_price_dates_from'] : get_post_meta( $id, '_sale_price_dates_from', true ); - $date_to = isset( $data['sale_price_dates_to'] ) ? $data['sale_price_dates_to'] : get_post_meta( $id, '_sale_price_dates_to', true ); - - // Dates - if ( $date_from ) { - update_post_meta( $id, '_sale_price_dates_from', strtotime( $date_from ) ); - } else { - update_post_meta( $id, '_sale_price_dates_from', '' ); - } - - if ( $date_to ) { - update_post_meta( $id, '_sale_price_dates_to', strtotime( $date_to ) ); - } else { - update_post_meta( $id, '_sale_price_dates_to', '' ); - } - - if ( $date_to && ! $date_from ) { - update_post_meta( $id, '_sale_price_dates_from', strtotime( 'NOW', current_time( 'timestamp' ) ) ); - } - - // Update price if on sale - if ( '' !== $sale_price && '' == $date_to && '' == $date_from ) { - update_post_meta( $id, '_price', wc_format_decimal( $sale_price ) ); - } else { - update_post_meta( $id, '_price', $regular_price ); - } - - if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $id, '_price', wc_format_decimal( $sale_price ) ); - } - - if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $id, '_price', $regular_price ); - update_post_meta( $id, '_sale_price_dates_from', '' ); - update_post_meta( $id, '_sale_price_dates_to', '' ); - } - } - - // Update parent if grouped so price sorting works and stays in sync with the cheapest child - $_product = get_product( $id ); - if ( $_product->post->post_parent > 0 || $product_type == 'grouped' ) { - - $clear_parent_ids = array(); - - if ( $_product->post->post_parent > 0 ) { - $clear_parent_ids[] = $_product->post->post_parent; - } - - if ( $product_type == 'grouped' ) { - $clear_parent_ids[] = $id; - } - - if ( $clear_parent_ids ) { - foreach ( $clear_parent_ids as $clear_id ) { - - $children_by_price = get_posts( array( - 'post_parent' => $clear_id, - 'orderby' => 'meta_value_num', - 'order' => 'asc', - 'meta_key' => '_price', - 'posts_per_page' => 1, - 'post_type' => 'product', - 'fields' => 'ids' - ) ); - - if ( $children_by_price ) { - foreach ( $children_by_price as $child ) { - $child_price = get_post_meta( $child, '_price', true ); - update_post_meta( $clear_id, '_price', $child_price ); - } - } - } - } - } - - // Sold Individually - if ( isset( $data['sold_individually'] ) ) { - update_post_meta( $id, '_sold_individually', ( true === $data['sold_individually'] ) ? 'yes' : '' ); - } - - // Stock status - if ( isset( $data['in_stock'] ) ) { - $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; - } else { - $stock_status = get_post_meta( $id, '_stock_status', true ); - - if ( '' === $stock_status ) { - $stock_status = 'instock'; - } - } - - // Stock Data - if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { - // Manage stock - if ( isset( $data['managing_stock'] ) ) { - $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; - update_post_meta( $id, '_manage_stock', $managing_stock ); - } else { - $managing_stock = get_post_meta( $id, '_manage_stock', true ); - } - - // Backorders - if ( isset( $data['backorders'] ) ) { - if ( 'notify' == $data['backorders'] ) { - $backorders = 'notify'; - } else { - $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; - } - - update_post_meta( $id, '_backorders', $backorders ); - } else { - $backorders = get_post_meta( $id, '_backorders', true ); - } - - if ( 'grouped' == $product_type ) { - - update_post_meta( $id, '_manage_stock', 'no' ); - update_post_meta( $id, '_backorders', 'no' ); - update_post_meta( $id, '_stock', '' ); - - wc_update_product_stock_status( $id, $stock_status ); - - } elseif ( 'external' == $product_type ) { - - update_post_meta( $id, '_manage_stock', 'no' ); - update_post_meta( $id, '_backorders', 'no' ); - update_post_meta( $id, '_stock', '' ); - - wc_update_product_stock_status( $id, 'instock' ); - - } elseif ( 'yes' == $managing_stock ) { - update_post_meta( $id, '_backorders', $backorders ); - - wc_update_product_stock_status( $id, $stock_status ); - - // Stock quantity - if ( isset( $data['stock_quantity'] ) ) { - wc_update_product_stock( $id, intval( $data['stock_quantity'] ) ); - } - } else { - - // Don't manage stock - update_post_meta( $id, '_manage_stock', 'no' ); - update_post_meta( $id, '_backorders', $backorders ); - update_post_meta( $id, '_stock', '' ); - - wc_update_product_stock_status( $id, $stock_status ); - } - - } else { - wc_update_product_stock_status( $id, $stock_status ); - } - - // Upsells - if ( isset( $data['upsell_ids'] ) ) { - $upsells = array(); - $ids = $data['upsell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $upsells[] = $id; - } - } - - update_post_meta( $id, '_upsell_ids', $upsells ); - } else { - delete_post_meta( $id, '_upsell_ids' ); - } - } - - // Cross sells - if ( isset( $data['cross_sell_ids'] ) ) { - $crosssells = array(); - $ids = $data['cross_sell_ids']; - - if ( ! empty( $ids ) ) { - foreach ( $ids as $id ) { - if ( $id && $id > 0 ) { - $crosssells[] = $id; - } - } - - update_post_meta( $id, '_crosssell_ids', $crosssells ); - } else { - delete_post_meta( $id, '_crosssell_ids' ); - } - } - - // Product categories - if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { - $terms = array_map( 'wc_clean', $data['categories'] ); - wp_set_object_terms( $id, $terms, 'product_cat' ); - } - - // Product tags - if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { - $terms = array_map( 'wc_clean', $data['tags'] ); - wp_set_object_terms( $id, $terms, 'product_tag' ); - } - - // Downloadable - if ( isset( $data['downloadable'] ) ) { - $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; - update_post_meta( $id, '_downloadable', $is_downloadable ); - } else { - $is_downloadable = get_post_meta( $id, '_downloadable', true ); - } - - // Downloadable options - if ( 'yes' == $is_downloadable ) { - - // Downloadable files - if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { - $this->save_downloadable_files( $id, $data['downloads'] ); - } - - // Download limit - if ( isset( $data['download_limit'] ) ) { - $download_limit = absint( $data['download_limit'] ); - update_post_meta( $id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit ); - } - - // Download expiry - if ( isset( $data['download_expiry'] ) ) { - $download_expiry = absint( $data['download_expiry'] ); - update_post_meta( $id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry ); - } - - // Download type - if ( isset( $data['download_type'] ) ) { - update_post_meta( $id, '_download_type', wc_clean( $data['download_type'] ) ); - } - } - - // Product url - if ( $product_type == 'external' ) { - if ( isset( $data['product_url'] ) ) { - update_post_meta( $id, '_product_url', wc_clean( $data['product_url'] ) ); - } - - if ( isset( $data['button_text'] ) ) { - update_post_meta( $id, '_button_text', wc_clean( $data['button_text'] ) ); - } - } - - // Do action for product type - do_action( 'woocommerce_api_process_product_meta_' . $product_type, $id, $data ); - - return true; - } - - /** - * Save variations - * - * @since 2.2 - * @param int $id - * @param array $data - * @return bool|WP_Error - */ - protected function save_variations( $id, $data ) { - global $wpdb; - - $variations = $data['variations']; - $attributes = (array) maybe_unserialize( get_post_meta( $id, '_product_attributes', true ) ); - - foreach ( $variations as $menu_order => $variation ) { - $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; - - // Generate a useful post title - $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $id ) ) ); - - // Update or Add post - if ( ! $variation_id ) { - $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; - - $new_variation = array( - 'post_title' => $variation_post_title, - 'post_content' => '', - 'post_status' => $post_status, - 'post_author' => get_current_user_id(), - 'post_parent' => $id, - 'post_type' => 'product_variation', - 'menu_order' => $menu_order - ); - - $variation_id = wp_insert_post( $new_variation ); - - do_action( 'woocommerce_create_product_variation', $variation_id ); - } else { - $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); - if ( isset( $variation['visible'] ) ) { - $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; - $update_variation['post_status'] = $post_status; - } - - $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); - - do_action( 'woocommerce_update_product_variation', $variation_id ); - } - - // Stop with we don't have a variation ID - if ( is_wp_error( $variation_id ) ) { - return $variation_id; - } - - // SKU - if ( isset( $variation['sku'] ) ) { - $sku = get_post_meta( $variation_id, '_sku', true ); - $new_sku = wc_clean( $variation['sku'] ); - - if ( '' == $new_sku ) { - update_post_meta( $variation_id, '_sku', '' ); - } elseif ( $new_sku !== $sku ) { - if ( ! empty( $new_sku ) ) { - $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); - if ( ! $unique_sku ) { - return new WP_Error( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product' ), array( 'status' => 400 ) ); - } else { - update_post_meta( $variation_id, '_sku', $new_sku ); - } - } else { - update_post_meta( $variation_id, '_sku', '' ); - } - } - } - - // Thumbnail - if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { - $image = current( $variation['image'] ); - if ( $image && is_array( $image ) ) { - if ( isset( $image['position'] ) && isset( $image['src'] ) && $image['position'] == 0 ) { - $upload = $this->upload_product_image( wc_clean( $image['src'] ) ); - if ( is_wp_error( $upload ) ) { - return new WP_Error( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), array( 'status' => 400 ) ); - } - $attachment_id = $this->set_product_image_as_attachment( $upload, $id ); - update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); - } - } else { - delete_post_meta( $variation_id, '_thumbnail_id' ); - } - } - - // Virtual variation - if ( isset( $variation['virtual'] ) ) { - $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no'; - update_post_meta( $variation_id, '_virtual', $is_virtual ); - } - - // Downloadable variation - if ( isset( $variation['downloadable'] ) ) { - $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no'; - update_post_meta( $variation_id, '_downloadable', $is_downloadable ); - } else { - $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); - } - - // Shipping data - $this->save_product_shipping_data( $variation_id, $variation ); - - // Stock handling - if ( isset( $variation['stock_quantity'] ) ) { - wc_update_product_stock( $variation_id, intval( $variation['stock_quantity'] ) ); - } - - // Backorders - if ( isset( $variation['backorders'] ) ) { - if ( $variation['backorders'] !== 'parent' ) { - if ( 'notify' == $variation['backorders'] ) { - $backorders = 'notify'; - } else { - $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no'; - } - - update_post_meta( $variation_id, '_backorders', $backorders ); - } else { - delete_post_meta( $variation_id, '_backorders' ); - } - } - - // Regular Price - if ( isset( $variation['regular_price'] ) ) { - $regular_price = ( '' === $variation['regular_price'] ) ? '' : wc_format_decimal( $variation['regular_price'] ); - update_post_meta( $variation_id, '_regular_price', $regular_price ); - } else { - $regular_price = get_post_meta( $variation_id, '_regular_price', true ); - } - - // Sale Price - if ( isset( $variation['sale_price'] ) ) { - $sale_price = ( '' === $variation['sale_price'] ) ? '' : wc_format_decimal( $variation['sale_price'] ); - update_post_meta( $variation_id, '_sale_price', $sale_price ); - } else { - $sale_price = get_post_meta( $variation_id, '_sale_price', true ); - } - - $date_from = isset( $variation['sale_price_dates_from'] ) ? $variation['sale_price_dates_from'] : get_post_meta( $variation_id, '_sale_price_dates_from', true ); - $date_to = isset( $variation['sale_price_dates_to'] ) ? $variation['sale_price_dates_to'] : get_post_meta( $variation_id, '_sale_price_dates_to', true ); - - // Save Dates - if ( $date_from ) { - update_post_meta( $variation_id, '_sale_price_dates_from', strtotime( $date_from ) ); - } else { - update_post_meta( $variation_id, '_sale_price_dates_from', '' ); - } - - if ( $date_to ) { - update_post_meta( $variation_id, '_sale_price_dates_to', strtotime( $date_to ) ); - } else { - update_post_meta( $variation_id, '_sale_price_dates_to', '' ); - } - - if ( $date_to && ! $date_from ) { - update_post_meta( $variation_id, '_sale_price_dates_from', strtotime( 'NOW', current_time( 'timestamp' ) ) ); - } - - // Update price if on sale - if ( '' != $sale_price && '' == $date_to && '' == $date_from ) { - update_post_meta( $variation_id, '_price', $sale_price ); - } else { - update_post_meta( $variation_id, '_price', $regular_price ); - } - - if ( '' != $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $variation_id, '_price', $sale_price ); - } - - if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { - update_post_meta( $variation_id, '_price', $regular_price ); - update_post_meta( $variation_id, '_sale_price_dates_from', '' ); - update_post_meta( $variation_id, '_sale_price_dates_to', '' ); - } - - // Tax class - if ( isset( $variation['tax_class'] ) ) { - if ( $variation['tax_class'] !== 'parent' ) { - update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); - } else { - delete_post_meta( $variation_id, '_tax_class' ); - } - } - - // Downloads - if ( 'yes' == $is_downloadable ) { - // Downloadable files - if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { - $this->save_downloadable_files( $id, $variation['downloads'], $variation_id ); - } - - // Download limit - if ( isset( $variation['download_limit'] ) ) { - $download_limit = absint( $variation['download_limit'] ); - update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit ); - } - - // Download expiry - if ( isset( $variation['download_expiry'] ) ) { - $download_expiry = absint( $variation['download_expiry'] ); - update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry ); - } - } else { - update_post_meta( $variation_id, '_download_limit', '' ); - update_post_meta( $variation_id, '_download_expiry', '' ); - update_post_meta( $variation_id, '_downloadable_files', '' ); - } - - // Update taxonomies - if ( isset( $variation['attributes'] ) ) { - $updated_attribute_keys = array(); - - foreach ( $variation['attributes'] as $attribute_key => $attribute ) { - - if ( ! isset( $attribute['name'] ) ) { - continue; - } - - $taxonomy = $this->get_attribute_taxonomy_by_label( $attribute['name'] ); - - if ( isset( $attributes[ $taxonomy ] ) ) { - $_attribute = $attributes[ $taxonomy ]; - - if ( $_attribute['is_variation'] ) { - $attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] ); - $attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; - $updated_attribute_keys[] = $attribute_key; - update_post_meta( $variation_id, $attribute_key, $attribute_value ); - } - } - } - - // Remove old taxonomies attributes so data is kept up to date - first get attribute key names - $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); - - foreach ( $delete_attribute_keys as $key ) { - delete_post_meta( $variation_id, $key ); - } - } - - do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); - } - - // Update parent if variable so price sorting works and stays in sync with the cheapest child - WC_Product_Variable::sync( $id ); - - // Update default attribute options setting - if ( isset( $data['default_attribute'] ) && is_array( $data['default_attribute'] ) ) { - $default_attributes = array(); - - foreach ( $data['default_attribute'] as $default_attr_key => $default_attr ) { - if ( ! isset( $default_attr['name'] ) ) { - continue; - } - - $taxonomy = $this->get_attribute_taxonomy_by_label( $default_attr['name'] ); - - if ( isset( $attributes[ $taxonomy ] ) ) { - $_attribute = $attributes[ $taxonomy ]; - - if ( $_attribute['is_variation'] ) { - // Don't use wc_clean as it destroys sanitized characters - if ( isset( $default_attr['option'] ) ) { - $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); - } else { - $value = ''; - } - - if ( $value ) { - $default_attributes[ $taxonomy ] = $value; - } - } - } - } - - update_post_meta( $id, '_default_attributes', $default_attributes ); - } - - return true; - } - - /** - * Save product shipping data - * - * @since 2.2 - * @param int $id - * @param array $data - * @return void - */ - private function save_product_shipping_data( $id, $data ) { - if ( isset( $data['weight'] ) ) { - update_post_meta( $id, '_weight', ( '' === $data['weight'] ) ? '' : wc_format_decimal( $data['weight'] ) ); - } - - // Product dimensions - if ( isset( $data['dimensions'] ) ) { - // Height - if ( isset( $data['dimensions']['height'] ) ) { - update_post_meta( $id, '_height', ( '' === $data['dimensions']['height'] ) ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); - } - - // Width - if ( isset( $data['dimensions']['width'] ) ) { - update_post_meta( $id, '_width', ( '' === $data['dimensions']['width'] ) ? '' : wc_format_decimal($data['dimensions']['width'] ) ); - } - - // Length - if ( isset( $data['dimensions']['length'] ) ) { - update_post_meta( $id, '_length', ( '' === $data['dimensions']['length'] ) ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); - } - } - - // Virtual - if ( isset( $data['virtual'] ) ) { - $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; - - if ( 'yes' == $virtual ) { - update_post_meta( $id, '_weight', '' ); - update_post_meta( $id, '_length', '' ); - update_post_meta( $id, '_width', '' ); - update_post_meta( $id, '_height', '' ); - } - } - - // Shipping class - if ( isset( $data['shipping_class'] ) ) { - wp_set_object_terms( $id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); - } - } - - /** - * Save downloadable files - * - * @since 2.2 - * @param int $product_id - * @param array $downloads - * @param int $variation_id - * @return void - */ - private function save_downloadable_files( $product_id, $downloads, $variation_id = 0 ) { - $files = array(); - - // file paths will be stored in an array keyed off md5(file path) - foreach ( $downloads as $key => $file ) { - if ( ! isset( $file['url'] ) ) { - continue; - } - - $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; - $file_url = wc_clean( $file['url'] ); - - $files[ md5( $file_url ) ] = array( - 'name' => $file_name, - 'file' => $file_url - ); - } - - // grant permission to any newly added files on any existing orders for this product prior to saving - do_action( 'woocommerce_process_product_file_download_paths', $product_id, $variation_id, $files ); - - update_post_meta( $product_id, '_downloadable_files', $files ); - } - - /** - * Get attribute taxonomy by label. - * - * @since 2.2 - * @param string $label - * @return stdClass - */ - private function get_attribute_taxonomy_by_label( $label ) { - $taxonomy = null; - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - foreach ( $attribute_taxonomies as $key => $tax ) { - if ( $label == $tax->attribute_label ) { - $taxonomy = 'pa_' . $tax->attribute_name; - - break; - } - } - - return $taxonomy; - } - - /** - * Get the images for a product or product variation - * - * @since 2.1 - * @param WC_Product|WC_Product_Variation $product - * @return array - */ - private function get_images( $product ) { - - $images = $attachment_ids = array(); - - if ( $product->is_type( 'variation' ) ) { - - if ( has_post_thumbnail( $product->get_variation_id() ) ) { - - // add variation image if set - $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() ); - - } elseif ( has_post_thumbnail( $product->id ) ) { - - // otherwise use the parent product featured image if set - $attachment_ids[] = get_post_thumbnail_id( $product->id ); - } - - } else { - - // add featured image - if ( has_post_thumbnail( $product->id ) ) { - $attachment_ids[] = get_post_thumbnail_id( $product->id ); - } - - // add gallery images - $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_ids() ); - } - - // build image data - foreach ( $attachment_ids as $position => $attachment_id ) { - - $attachment_post = get_post( $attachment_id ); - - if ( is_null( $attachment_post ) ) { - continue; - } - - $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); - - if ( ! is_array( $attachment ) ) { - continue; - } - - $images[] = array( - 'id' => (int) $attachment_id, - 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), - 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), - 'src' => current( $attachment ), - 'title' => get_the_title( $attachment_id ), - 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), - 'position' => $position, - ); - } - - // set a placeholder image if the product has no images set - if ( empty( $images ) ) { - - $images[] = array( - 'id' => 0, - 'created_at' => $this->server->format_datetime( time() ), // default to now - 'updated_at' => $this->server->format_datetime( time() ), - 'src' => wc_placeholder_img_src(), - 'title' => __( 'Placeholder', 'woocommerce' ), - 'alt' => __( 'Placeholder', 'woocommerce' ), - 'position' => 0, - ); - } - - return $images; - } - - /** - * Save product images - * - * @since 2.2 - * @param array $images - * @param int $id - * @return void|WP_Error - */ - protected function save_product_images( $id, $images ) { - if ( is_array( $images ) ) { - $gallery = array(); - - foreach ( $images as $image ) { - if ( isset( $image['position'] ) && $image['position'] == 0 ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = $this->upload_product_image( wc_clean( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - return new WP_Error( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), array( 'status' => 400 ) ); - } - - $attachment_id = $this->set_product_image_as_attachment( $upload, $id ); - } - - set_post_thumbnail( $id, $attachment_id ); - } else { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = $this->upload_product_image( wc_clean( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - return new WP_Error( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), array( 'status' => 400 ) ); - } - } - - $gallery[] = $this->set_product_image_as_attachment( $upload, $id ); - } - } - - if ( ! empty( $gallery ) ) { - update_post_meta( $id, '_product_image_gallery', implode( ',', $gallery ) ); - } - } else { - delete_post_thumbnail( $id ); - update_post_meta( $id, '_product_image_gallery', '' ); - } - } - - /** - * Upload image from URL - * - * @since 2.2 - * @param string $image_url - * @return integer attachment id - */ - public function upload_product_image( $image_url ) { - $file_name = basename( current( explode( '?', $image_url ) ) ); - $wp_filetype = wp_check_filetype( $file_name, null ); - $parsed_url = @parse_url( $image_url ); - - // Check parsed URL - if ( ! $parsed_url || ! is_array( $parsed_url ) ) { - return new WP_Error( 'woocommerce_api_invalid_product_image', sprintf( __( 'Invalid URL %s' ), $image_url ), array( 'status' => 400 ) ); - } - - // Ensure url is valid - $image_url = str_replace( ' ', '%20', $image_url ); - - // Get the file - $response = wp_remote_get( $image_url, array( - 'timeout' => 10 - ) ); - - if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { - return new WP_Error( 'woocommerce_api_invalid_remote_product_image', sprintf( __( 'Error getting remote image %s' ), $image_url ), array( 'status' => 400 ) ); - } - - // Ensure we have a file name and type - if ( ! $wp_filetype['type'] ) { - $headers = wp_remote_retrieve_headers( $response ); - if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) { - $disposition = end( explode( 'filename=', $headers['content-disposition'] ) ); - $disposition = sanitize_file_name( $disposition ); - $file_name = $disposition; - } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) { - $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] ); - } - unset( $headers ); - } - - // Upload the file - $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) ); - - if ( $upload['error'] ) { - return new WP_Error( 'woocommerce_api_product_image_upload_error', $upload['error'], array( 'status' => 400 ) ); - } - - // Get filesize - $filesize = filesize( $upload['file'] ); - - if ( 0 == $filesize ) { - @unlink( $upload['file'] ); - unset( $upload ); - return new WP_Error( 'woocommerce_api_product_image_upload_file_error', __( 'Zero size file downloaded', 'woocommerce' ), array( 'status' => 400 ) ); - } - - unset( $response ); - - return $upload; - } - - /** - * Get product image as attachment - * - * @since 2.2 - * @param array $upload - * @param int $id - * @return int - */ - protected function set_product_image_as_attachment( $upload, $id ) { - $info = wp_check_filetype( $upload['file'] ); - $title = ''; - $content = ''; - - if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { - if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { - $title = $image_meta['title']; - } - if ( trim( $image_meta['caption'] ) ) { - $content = $image_meta['caption']; - } - } - - $attachment = array( - 'post_mime_type' => $info['type'], - 'guid' => $upload['url'], - 'post_parent' => $id, - 'post_title' => $title, - 'post_content' => $content - ); - - $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); - if ( ! is_wp_error( $attachment_id ) ) { - wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); - } - - return $attachment_id; - } - - /** - * Get the attributes for a product or product variation - * - * @since 2.1 - * @param WC_Product|WC_Product_Variation $product - * @return array - */ - private function get_attributes( $product ) { - - $attributes = array(); - - if ( $product->is_type( 'variation' ) ) { - - // variation attributes - foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { - - // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` - $attributes[] = array( - 'name' => ucwords( str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ) ), - 'option' => $attribute, - ); - } - - } else { - - foreach ( $product->get_attributes() as $attribute ) { - - // taxonomy-based attributes are comma-separated, others are pipe (|) separated - if ( $attribute['is_taxonomy'] ) { - $options = explode( ',', $product->get_attribute( $attribute['name'] ) ); - } else { - $options = explode( '|', $product->get_attribute( $attribute['name'] ) ); - } - - $attributes[] = array( - 'name' => ucwords( str_replace( 'pa_', '', $attribute['name'] ) ), - 'position' => $attribute['position'], - 'visible' => (bool) $attribute['is_visible'], - 'variation' => (bool) $attribute['is_variation'], - 'options' => array_map( 'trim', $options ), - ); - } - } - - return $attributes; - } - - /** - * Get the downloads for a product or product variation - * - * @since 2.1 - * @param WC_Product|WC_Product_Variation $product - * @return array - */ - private function get_downloads( $product ) { - - $downloads = array(); - - if ( $product->is_downloadable() ) { - - foreach ( $product->get_files() as $file_id => $file ) { - - $downloads[] = array( - 'id' => $file_id, // do not cast as int as this is a hash - 'name' => $file['name'], - 'file' => $file['file'], - ); - } - } - - return $downloads; - } - -} diff --git a/includes/api/class-wc-api-reports.php b/includes/api/class-wc-api-reports.php deleted file mode 100644 index 8aa4ac5c9bb..00000000000 --- a/includes/api/class-wc-api-reports.php +++ /dev/null @@ -1,476 +0,0 @@ -base ] = array( - array( array( $this, 'get_reports' ), WC_API_Server::READABLE ), - ); - - # GET /reports/sales - $routes[ $this->base . '/sales'] = array( - array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ), - ); - - # GET /reports/sales/top_sellers - $routes[ $this->base . '/sales/top_sellers' ] = array( - array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ), - ); - - return $routes; - } - - /** - * Get a simple listing of available reports - * - * @since 2.1 - * @return array - */ - public function get_reports() { - - return array( 'reports' => array( 'sales', 'sales/top_sellers' ) ); - } - - /** - * Get the sales report - * - * @since 2.1 - * @param string $fields fields to include in response - * @param array $filter date filtering - * @return array - */ - public function get_sales_report( $fields = null, $filter = array() ) { - - // check user permissions - $check = $this->validate_request(); - - if ( is_wp_error( $check ) ) - return $check; - - // set date filtering - $this->setup_report( $filter ); - - // total sales, taxes, shipping, and order count - $totals = $this->report->get_order_report_data( array( - 'data' => array( - '_order_total' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'sales' - ), - '_order_tax' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'tax' - ), - '_order_shipping_tax' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'shipping_tax' - ), - '_order_shipping' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'shipping' - ), - 'ID' => array( - 'type' => 'post_data', - 'function' => 'COUNT', - 'name' => 'order_count' - ) - ), - 'filter_range' => true, - ) ); - - // total items ordered - $total_items = absint( $this->report->get_order_report_data( array( - 'data' => array( - '_qty' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'line_item', - 'function' => 'SUM', - 'name' => 'order_item_qty' - ) - ), - 'query_type' => 'get_var', - 'filter_range' => true, - ) ) ); - - // total discount used - $total_discount = $this->report->get_order_report_data( array( - 'data' => array( - 'discount_amount' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'coupon', - 'function' => 'SUM', - 'name' => 'discount_amount' - ) - ), - 'where' => array( - array( - 'key' => 'order_item_type', - 'value' => 'coupon', - 'operator' => '=' - ) - ), - 'query_type' => 'get_var', - 'filter_range' => true, - ) ); - - // new customers - $users_query = new WP_User_Query( - array( - 'fields' => array( 'user_registered' ), - 'role' => 'customer', - ) - ); - - $customers = $users_query->get_results(); - - foreach ( $customers as $key => $customer ) { - if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) - unset( $customers[ $key ] ); - } - - $total_customers = count( $customers ); - - // get order totals grouped by period - $orders = $this->report->get_order_report_data( array( - 'data' => array( - '_order_total' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_sales' - ), - '_order_shipping' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_shipping' - ), - '_order_tax' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_tax' - ), - '_order_shipping_tax' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_shipping_tax' - ), - 'ID' => array( - 'type' => 'post_data', - 'function' => 'COUNT', - 'name' => 'total_orders', - 'distinct' => true, - ), - 'post_date' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'post_date' - ), - ), - 'group_by' => $this->report->group_by_query, - 'order_by' => 'post_date ASC', - 'query_type' => 'get_results', - 'filter_range' => true, - ) ); - - // get order item totals grouped by period - $order_items = $this->report->get_order_report_data( array( - 'data' => array( - '_qty' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'line_item', - 'function' => 'SUM', - 'name' => 'order_item_count' - ), - 'post_date' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'post_date' - ), - ), - 'where' => array( - array( - 'key' => 'order_item_type', - 'value' => 'line_item', - 'operator' => '=' - ) - ), - 'group_by' => $this->report->group_by_query, - 'order_by' => 'post_date ASC', - 'query_type' => 'get_results', - 'filter_range' => true, - ) ); - - // get discount totals grouped by period - $discounts = $this->report->get_order_report_data( array( - 'data' => array( - 'discount_amount' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'coupon', - 'function' => 'SUM', - 'name' => 'discount_amount' - ), - 'post_date' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'post_date' - ), - ), - 'where' => array( - array( - 'key' => 'order_item_type', - 'value' => 'coupon', - 'operator' => '=' - ) - ), - 'group_by' => $this->report->group_by_query . ', order_item_name', - 'order_by' => 'post_date ASC', - 'query_type' => 'get_results', - 'filter_range' => true, - ) ); - - $period_totals = array(); - - // setup period totals by ensuring each period in the interval has data - for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { - - switch ( $this->report->chart_groupby ) { - case 'day' : - $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); - break; - case 'month' : - $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); - break; - } - - // set the customer signups for each period - $customer_count = 0; - foreach ( $customers as $customer ) { - - if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { - $customer_count++; - } - } - - $period_totals[ $time ] = array( - 'sales' => wc_format_decimal( 0.00, 2 ), - 'orders' => 0, - 'items' => 0, - 'tax' => wc_format_decimal( 0.00, 2 ), - 'shipping' => wc_format_decimal( 0.00, 2 ), - 'discount' => wc_format_decimal( 0.00, 2 ), - 'customers' => $customer_count, - ); - } - - // add total sales, total order count, total tax and total shipping for each period - foreach ( $orders as $order ) { - - $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); - - if ( ! isset( $period_totals[ $time ] ) ) - continue; - - $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); - $period_totals[ $time ]['orders'] = (int) $order->total_orders; - $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); - $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); - } - - // add total order items for each period - foreach ( $order_items as $order_item ) { - - $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); - - if ( ! isset( $period_totals[ $time ] ) ) - continue; - - $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; - } - - // add total discount for each period - foreach ( $discounts as $discount ) { - - $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); - - if ( ! isset( $period_totals[ $time ] ) ) - continue; - - $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); - } - - $sales_data = array( - 'total_sales' => wc_format_decimal( $totals->sales, 2 ), - 'average_sales' => wc_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ), - 'total_orders' => (int) $totals->order_count, - 'total_items' => $total_items, - 'total_tax' => wc_format_decimal( $totals->tax + $totals->shipping_tax, 2 ), - 'total_shipping' => wc_format_decimal( $totals->shipping, 2 ), - 'total_discount' => is_null( $total_discount ) ? wc_format_decimal( 0.00, 2 ) : wc_format_decimal( $total_discount, 2 ), - 'totals_grouped_by' => $this->report->chart_groupby, - 'totals' => $period_totals, - 'total_customers' => $total_customers, - ); - - return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) ); - } - - /** - * Get the top sellers report - * - * @since 2.1 - * @param string $fields fields to include in response - * @param array $filter date filtering - * @return array - */ - public function get_top_sellers_report( $fields = null, $filter = array() ) { - - // check user permissions - $check = $this->validate_request(); - - if ( is_wp_error( $check ) ) { - return $check; - } - - // set date filtering - $this->setup_report( $filter ); - - $top_sellers = $this->report->get_order_report_data( array( - 'data' => array( - '_product_id' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'line_item', - 'function' => '', - 'name' => 'product_id' - ), - '_qty' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'line_item', - 'function' => 'SUM', - 'name' => 'order_item_qty' - ) - ), - 'order_by' => 'order_item_qty DESC', - 'group_by' => 'product_id', - 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, - 'query_type' => 'get_results', - 'filter_range' => true, - ) ); - - $top_sellers_data = array(); - - foreach ( $top_sellers as $top_seller ) { - - $product = get_product( $top_seller->product_id ); - - $top_sellers_data[] = array( - 'title' => $product->get_title(), - 'product_id' => $top_seller->product_id, - 'quantity' => $top_seller->order_item_qty, - ); - } - - return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) ); - } - - /** - * Setup the report object and parse any date filtering - * - * @since 2.1 - * @param array $filter date filtering - */ - private function setup_report( $filter ) { - - include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); - - $this->report = new WC_Admin_Report(); - - if ( empty( $filter['period'] ) ) { - - // custom date range - $filter['period'] = 'custom'; - - if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { - - // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges - $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] ); - $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null; - - } else { - - // default custom range to today - $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); - } - - } else { - - // ensure period is valid - if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { - $filter['period'] = 'week'; - } - - // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods - // allow "week" for period instead of "7day" - if ( 'week' === $filter['period'] ) { - $filter['period'] = '7day'; - } - } - - $this->report->calculate_current_range( $filter['period'] ); - } - - /** - * Verify that the current user has permission to view reports - * - * @since 2.1 - * @see WC_API_Resource::validate_request() - * @param null $id unused - * @param null $type unused - * @param null $context unused - * @return bool true if the request is valid and should be processed, false otherwise - */ - protected function validate_request( $id = null, $type = null, $context = null ) { - - if ( ! current_user_can( 'view_woocommerce_reports' ) ) { - - return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) ); - - } else { - - return true; - } - } -} diff --git a/includes/api/class-wc-api-resource.php b/includes/api/class-wc-api-resource.php deleted file mode 100644 index 996d8d50379..00000000000 --- a/includes/api/class-wc-api-resource.php +++ /dev/null @@ -1,411 +0,0 @@ -server = $server; - - // automatically register routes for sub-classes - add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); - - // remove fields from responses when requests specify certain fields - // note these are hooked at a later priority so data added via filters (e.g. customer data to the order response) - // still has the fields filtered properly - foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { - - add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); - add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 ); - } - } - - /** - * Validate the request by checking: - * - * 1) the ID is a valid integer - * 2) the ID returns a valid post object and matches the provided post type - * 3) the current user has the proper permissions to read/edit/delete the post - * - * @since 2.1 - * @param string|int $id the post ID - * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` - * @param string $context the context of the request, either `read`, `edit` or `delete` - * @return int|WP_Error valid post ID or WP_Error if any of the checks fails - */ - protected function validate_request( $id, $type, $context ) { - - if ( 'shop_order' === $type || 'shop_coupon' === $type ) - $resource_name = str_replace( 'shop_', '', $type ); - else - $resource_name = $type; - - $id = absint( $id ); - - // validate ID - if ( empty( $id ) ) - return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); - - // only custom post types have per-post type/permission checks - if ( 'customer' !== $type ) { - - $post = get_post( $id ); - - // for checking permissions, product variations are the same as the product post type - $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; - - // validate post type - if ( $type !== $post_type ) - return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); - - // validate permissions - switch ( $context ) { - - case 'read': - if ( ! $this->is_readable( $post ) ) - return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); - break; - - case 'edit': - if ( ! $this->is_editable( $post ) ) - return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); - break; - - case 'delete': - if ( ! $this->is_deletable( $post ) ) - return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); - break; - } - } - - return $id; - } - - /** - * Add common request arguments to argument list before WP_Query is run - * - * @since 2.1 - * @param array $base_args required arguments for the query (e.g. `post_type`, etc) - * @param array $request_args arguments provided in the request - * @return array - */ - protected function merge_query_args( $base_args, $request_args ) { - - $args = array(); - - // date - if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { - - $args['date_query'] = array(); - - // resources created after specified date - if ( ! empty( $request_args['created_at_min'] ) ) - $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); - - // resources created before specified date - if ( ! empty( $request_args['created_at_max'] ) ) - $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); - - // resources updated after specified date - if ( ! empty( $request_args['updated_at_min'] ) ) - $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); - - // resources updated before specified date - if ( ! empty( $request_args['updated_at_max'] ) ) - $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); - } - - // search - if ( ! empty( $request_args['q'] ) ) - $args['s'] = $request_args['q']; - - // resources per response - if ( ! empty( $request_args['limit'] ) ) - $args['posts_per_page'] = $request_args['limit']; - - // resource offset - if ( ! empty( $request_args['offset'] ) ) - $args['offset'] = $request_args['offset']; - - // allow order change (ASC or DESC) - if ( ! empty( $request_args['order'] ) ) { - $args['order'] = $request_args['order']; - unset( $request_args['order'] ); - } - - // allow post status change - if ( ! empty( $request_args['post_status'] ) ) { - $args['post_status'] = $request_args['post_status']; - unset( $request_args['post_status'] ); - } - - // resource page - $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; - - return array_merge( $base_args, $args ); - } - - /** - * Add meta to resources when requested by the client. Meta is added as a top-level - * `_meta` attribute (e.g. `order_meta`) as a list of key/value pairs - * - * @since 2.1 - * @param array $data the resource data - * @param object $resource the resource object (e.g WC_Order) - * @return mixed - */ - public function maybe_add_meta( $data, $resource ) { - - if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { - - // don't attempt to add meta more than once - if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) - return $data; - - // define the top-level property name for the meta - switch ( get_class( $resource ) ) { - - case 'WC_Order': - $meta_name = 'order_meta'; - break; - - case 'WC_Coupon': - $meta_name = 'coupon_meta'; - break; - - case 'WP_User': - $meta_name = 'customer_meta'; - break; - - default: - $meta_name = 'product_meta'; - break; - } - - if ( is_a( $resource, 'WP_User' ) ) { - - // customer meta - $meta = (array) get_user_meta( $resource->ID ); - - } elseif ( is_a( $resource, 'WC_Product_Variation' ) ) { - - // product variation meta - $meta = (array) get_post_meta( $resource->get_variation_id() ); - - } else { - - // coupon/order/product meta - $meta = (array) get_post_meta( $resource->id ); - } - - foreach( $meta as $meta_key => $meta_value ) { - - // don't add hidden meta by default - if ( ! is_protected_meta( $meta_key ) ) { - $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); - } - } - - } - - return $data; - } - - /** - * Restrict the fields included in the response if the request specified certain only certain fields should be returned - * - * @since 2.1 - * @param array $data the response data - * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order - * @param array|string the requested list of fields to include in the response - * @return array response data - */ - public function filter_response_fields( $data, $resource, $fields ) { - - if ( ! is_array( $data ) || empty( $fields ) ) - return $data; - - $fields = explode( ',', $fields ); - $sub_fields = array(); - - // get sub fields - foreach ( $fields as $field ) { - - if ( false !== strpos( $field, '.' ) ) { - - list( $name, $value ) = explode( '.', $field ); - - $sub_fields[ $name ] = $value; - } - } - - // iterate through top-level fields - foreach ( $data as $data_field => $data_value ) { - - // if a field has sub-fields and the top-level field has sub-fields to filter - if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { - - // iterate through each sub-field - foreach ( $data_value as $sub_field => $sub_field_value ) { - - // remove non-matching sub-fields - if ( ! in_array( $sub_field, $sub_fields ) ) { - unset( $data[ $data_field ][ $sub_field ] ); - } - } - - } else { - - // remove non-matching top-level fields - if ( ! in_array( $data_field, $fields ) ) { - unset( $data[ $data_field ] ); - } - } - } - - return $data; - } - - /** - * Delete a given resource - * - * @since 2.1 - * @param int $id the resource ID - * @param string $type the resource post type, or `customer` - * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) - * @return array|WP_Error - */ - protected function delete( $id, $type, $force = false ) { - - if ( 'shop_order' === $type || 'shop_coupon' === $type ) - $resource_name = str_replace( 'shop_', '', $type ); - else - $resource_name = $type; - - if ( 'customer' === $type ) { - - $result = wp_delete_user( $id ); - - if ( $result ) - return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); - else - return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); - - } else { - - // delete order/coupon/product - - $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); - - if ( ! $result ) - return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); - - if ( $force ) { - return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); - - } else { - - $this->server->send_status( '202' ); - - return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); - } - } - } - - - /** - * Checks if the given post is readable by the current user - * - * @since 2.1 - * @see WC_API_Resource::check_permission() - * @param WP_Post|int $post - * @return bool - */ - protected function is_readable( $post ) { - - return $this->check_permission( $post, 'read' ); - } - - /** - * Checks if the given post is editable by the current user - * - * @since 2.1 - * @see WC_API_Resource::check_permission() - * @param WP_Post|int $post - * @return bool - */ - protected function is_editable( $post ) { - - return $this->check_permission( $post, 'edit' ); - - } - - /** - * Checks if the given post is deletable by the current user - * - * @since 2.1 - * @see WC_API_Resource::check_permission() - * @param WP_Post|int $post - * @return bool - */ - protected function is_deletable( $post ) { - - return $this->check_permission( $post, 'delete' ); - } - - /** - * Checks the permissions for the current user given a post and context - * - * @since 2.1 - * @param WP_Post|int $post - * @param string $context the type of permission to check, either `read`, `write`, or `delete` - * @return bool true if the current user has the permissions to perform the context on the post - */ - private function check_permission( $post, $context ) { - - if ( ! is_a( $post, 'WP_Post' ) ) - $post = get_post( $post ); - - if ( is_null( $post ) ) - return false; - - $post_type = get_post_type_object( $post->post_type ); - - if ( 'read' === $context ) - return current_user_can( $post_type->cap->read_private_posts, $post->ID ); - - elseif ( 'edit' === $context ) - return current_user_can( $post_type->cap->edit_post, $post->ID ); - - elseif ( 'delete' === $context ) - return current_user_can( $post_type->cap->delete_post, $post->ID ); - - else - return false; - } - -} diff --git a/includes/api/class-wc-api-server.php b/includes/api/class-wc-api-server.php deleted file mode 100644 index 424f5486e4c..00000000000 --- a/includes/api/class-wc-api-server.php +++ /dev/null @@ -1,769 +0,0 @@ - self::METHOD_GET, - 'GET' => self::METHOD_GET, - 'POST' => self::METHOD_POST, - 'PUT' => self::METHOD_PUT, - 'PATCH' => self::METHOD_PATCH, - 'DELETE' => self::METHOD_DELETE, - ); - - /** - * Requested path (relative to the API root, wp-json.php) - * - * @var string - */ - public $path = ''; - - /** - * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE) - * - * @var string - */ - public $method = 'HEAD'; - - /** - * Request parameters - * - * This acts as an abstraction of the superglobals - * (GET => $_GET, POST => $_POST) - * - * @var array - */ - public $params = array( 'GET' => array(), 'POST' => array() ); - - /** - * Request headers - * - * @var array - */ - public $headers = array(); - - /** - * Request files (matches $_FILES) - * - * @var array - */ - public $files = array(); - - /** - * Request/Response handler, either JSON by default - * or XML if requested by client - * - * @var WC_API_Handler - */ - public $handler; - - - /** - * Setup class and set request/response handler - * - * @since 2.1 - * @param $path - * @return WC_API_Server - */ - public function __construct( $path ) { - - if ( empty( $path ) ) { - if ( isset( $_SERVER['PATH_INFO'] ) ) - $path = $_SERVER['PATH_INFO']; - else - $path = '/'; - } - - $this->path = $path; - $this->method = $_SERVER['REQUEST_METHOD']; - $this->params['GET'] = $_GET; - $this->params['POST'] = $_POST; - $this->headers = $this->get_headers( $_SERVER ); - $this->files = $_FILES; - - // Compatibility for clients that can't use PUT/PATCH/DELETE - if ( isset( $_GET['_method'] ) ) { - $this->method = strtoupper( $_GET['_method'] ); - } - - // determine type of request/response and load handler, JSON by default - if ( $this->is_json_request() ) - $handler_class = 'WC_API_JSON_Handler'; - - elseif ( $this->is_xml_request() ) - $handler_class = 'WC_API_XML_Handler'; - - else - $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this ); - - $this->handler = new $handler_class(); - } - - /** - * Check authentication for the request - * - * @since 2.1 - * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login - */ - public function check_authentication() { - - // allow plugins to remove default authentication or add their own authentication - $user = apply_filters( 'woocommerce_api_check_authentication', null, $this ); - - // API requests run under the context of the authenticated user - if ( is_a( $user, 'WP_User' ) ) - wp_set_current_user( $user->ID ); - - // WP_Errors are handled in serve_request() - elseif ( ! is_wp_error( $user ) ) - $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) ); - - return $user; - } - - /** - * Convert an error to an array - * - * This iterates over all error codes and messages to change it into a flat - * array. This enables simpler client behaviour, as it is represented as a - * list in JSON rather than an object/map - * - * @since 2.1 - * @param WP_Error $error - * @return array List of associative arrays with code and message keys - */ - protected function error_to_array( $error ) { - $errors = array(); - foreach ( (array) $error->errors as $code => $messages ) { - foreach ( (array) $messages as $message ) { - $errors[] = array( 'code' => $code, 'message' => $message ); - } - } - return array( 'errors' => $errors ); - } - - /** - * Handle serving an API request - * - * Matches the current server URI to a route and runs the first matching - * callback then outputs a JSON representation of the returned value. - * - * @since 2.1 - * @uses WC_API_Server::dispatch() - */ - public function serve_request() { - - do_action( 'woocommerce_api_server_before_serve', $this ); - - $this->header( 'Content-Type', $this->handler->get_content_type(), true ); - - // the API is enabled by default - if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) { - - $this->send_status( 404 ); - - echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) ); - - return; - } - - $result = $this->check_authentication(); - - // if authorization check was successful, dispatch the request - if ( ! is_wp_error( $result ) ) { - $result = $this->dispatch(); - } - - // handle any dispatch errors - if ( is_wp_error( $result ) ) { - $data = $result->get_error_data(); - if ( is_array( $data ) && isset( $data['status'] ) ) { - $this->send_status( $data['status'] ); - } - - $result = $this->error_to_array( $result ); - } - - // This is a filter rather than an action, since this is designed to be - // re-entrant if needed - $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this ); - - if ( ! $served ) { - - if ( 'HEAD' === $this->method ) - return; - - echo $this->handler->generate_response( $result ); - } - } - - /** - * Retrieve the route map - * - * The route map is an associative array with path regexes as the keys. The - * value is an indexed array with the callback function/method as the first - * item, and a bitmask of HTTP methods as the second item (see the class - * constants). - * - * Each route can be mapped to more than one callback by using an array of - * the indexed arrays. This allows mapping e.g. GET requests to one callback - * and POST requests to another. - * - * Note that the path regexes (array keys) must have @ escaped, as this is - * used as the delimiter with preg_match() - * - * @since 2.1 - * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` - */ - public function get_routes() { - - // index added by default - $endpoints = array( - - '/' => array( array( $this, 'get_index' ), self::READABLE ), - ); - - $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints ); - - // Normalise the endpoints - foreach ( $endpoints as $route => &$handlers ) { - if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) { - $handlers = array( $handlers ); - } - } - - return $endpoints; - } - - /** - * Match the request to a callback and call it - * - * @since 2.1 - * @return mixed The value returned by the callback, or a WP_Error instance - */ - public function dispatch() { - - switch ( $this->method ) { - - case 'HEAD': - case 'GET': - $method = self::METHOD_GET; - break; - - case 'POST': - $method = self::METHOD_POST; - break; - - case 'PUT': - $method = self::METHOD_PUT; - break; - - case 'PATCH': - $method = self::METHOD_PATCH; - break; - - case 'DELETE': - $method = self::METHOD_DELETE; - break; - - default: - return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) ); - } - - foreach ( $this->get_routes() as $route => $handlers ) { - foreach ( $handlers as $handler ) { - $callback = $handler[0]; - $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; - - if ( !( $supported & $method ) ) - continue; - - $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args ); - - if ( !$match ) - continue; - - if ( ! is_callable( $callback ) ) - return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) ); - - $args = array_merge( $args, $this->params['GET'] ); - if ( $method & self::METHOD_POST ) { - $args = array_merge( $args, $this->params['POST'] ); - } - if ( $supported & self::ACCEPT_DATA ) { - $data = $this->handler->parse_body( $this->get_raw_data() ); - $args = array_merge( $args, array( 'data' => $data ) ); - } - elseif ( $supported & self::ACCEPT_RAW_DATA ) { - $data = $this->get_raw_data(); - $args = array_merge( $args, array( 'data' => $data ) ); - } - - $args['_method'] = $method; - $args['_route'] = $route; - $args['_path'] = $this->path; - $args['_headers'] = $this->headers; - $args['_files'] = $this->files; - - $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback ); - - // Allow plugins to halt the request via this filter - if ( is_wp_error( $args ) ) { - return $args; - } - - $params = $this->sort_callback_params( $callback, $args ); - if ( is_wp_error( $params ) ) - return $params; - - return call_user_func_array( $callback, $params ); - } - } - - return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) ); - } - - /** - * urldecode deep. - * - * @since 2.2 - * @param string/array $value Data to decode with urldecode. - * @return string/array Decoded data. - */ - protected function urldecode_deep( $value ) { - if ( is_array( $value ) ) { - return array_map( array( $this, 'urldecode_deep' ), $value ); - } else { - return urldecode( $value ); - } - } - - /** - * Sort parameters by order specified in method declaration - * - * Takes a callback and a list of available params, then filters and sorts - * by the parameters the method actually needs, using the Reflection API - * - * @since 2.2 - * @param callable|array $callback the endpoint callback - * @param array $provided the provided request parameters - * @return array - */ - protected function sort_callback_params( $callback, $provided ) { - if ( is_array( $callback ) ) - $ref_func = new ReflectionMethod( $callback[0], $callback[1] ); - else - $ref_func = new ReflectionFunction( $callback ); - - $wanted = $ref_func->getParameters(); - $ordered_parameters = array(); - - foreach ( $wanted as $param ) { - if ( isset( $provided[ $param->getName() ] ) ) { - // We have this parameters in the list to choose from - if ( 'data' == $param->getName() ) { - $ordered_parameters[] = $provided[ $param->getName() ]; - continue; - } - - $ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] ); - } - elseif ( $param->isDefaultValueAvailable() ) { - // We don't have this parameter, but it's optional - $ordered_parameters[] = $param->getDefaultValue(); - } - else { - // We don't have this parameter and it wasn't optional, abort! - return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) ); - } - } - return $ordered_parameters; - } - - /** - * Get the site index. - * - * This endpoint describes the capabilities of the site. - * - * @since 2.1 - * @return array Index entity - */ - public function get_index() { - - // General site data - $available = array( 'store' => array( - 'name' => get_option( 'blogname' ), - 'description' => get_option( 'blogdescription' ), - 'URL' => get_option( 'siteurl' ), - 'wc_version' => WC()->version, - 'routes' => array(), - 'meta' => array( - 'timezone' => wc_timezone_string(), - 'currency' => get_woocommerce_currency(), - 'currency_format' => get_woocommerce_currency_symbol(), - 'tax_included' => ( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ), - 'weight_unit' => get_option( 'woocommerce_weight_unit' ), - 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), - 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ), - 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ), - 'links' => array( - 'help' => 'http://woothemes.github.io/woocommerce/rest-api/', - ), - ), - ) ); - - // Find the available routes - foreach ( $this->get_routes() as $route => $callbacks ) { - $data = array(); - - $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); - $methods = array(); - foreach ( self::$method_map as $name => $bitmask ) { - foreach ( $callbacks as $callback ) { - // Skip to the next route if any callback is hidden - if ( $callback[1] & self::HIDDEN_ENDPOINT ) - continue 3; - - if ( $callback[1] & $bitmask ) - $data['supports'][] = $name; - - if ( $callback[1] & self::ACCEPT_DATA ) - $data['accepts_data'] = true; - - // For non-variable routes, generate links - if ( strpos( $route, '<' ) === false ) { - $data['meta'] = array( - 'self' => get_woocommerce_api_url( $route ), - ); - } - } - } - $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data ); - } - return apply_filters( 'woocommerce_api_index', $available ); - } - - /** - * Send a HTTP status code - * - * @since 2.1 - * @param int $code HTTP status - */ - public function send_status( $code ) { - status_header( $code ); - } - - /** - * Send a HTTP header - * - * @since 2.1 - * @param string $key Header key - * @param string $value Header value - * @param boolean $replace Should we replace the existing header? - */ - public function header( $key, $value, $replace = true ) { - header( sprintf( '%s: %s', $key, $value ), $replace ); - } - - /** - * Send a Link header - * - * @internal The $rel parameter is first, as this looks nicer when sending multiple - * - * @link http://tools.ietf.org/html/rfc5988 - * @link http://www.iana.org/assignments/link-relations/link-relations.xml - * - * @since 2.1 - * @param string $rel Link relation. Either a registered type, or an absolute URL - * @param string $link Target IRI for the link - * @param array $other Other parameters to send, as an associative array - */ - public function link_header( $rel, $link, $other = array() ) { - - $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) ); - - foreach ( $other as $key => $value ) { - - if ( 'title' == $key ) { - - $value = '"' . $value . '"'; - } - - $header .= '; ' . $key . '=' . $value; - } - - $this->header( 'Link', $header, false ); - } - - /** - * Send pagination headers for resources - * - * @since 2.1 - * @param WP_Query|WP_User_Query $query - */ - public function add_pagination_headers( $query ) { - - // WP_User_Query - if ( is_a( $query, 'WP_User_Query' ) ) { - - $page = $query->page; - $single = count( $query->get_results() ) > 1; - $total = $query->get_total(); - $total_pages = $query->total_pages; - - // WP_Query - } else { - - $page = $query->get( 'paged' ); - $single = $query->is_single(); - $total = $query->found_posts; - $total_pages = $query->max_num_pages; - } - - if ( ! $page ) - $page = 1; - - $next_page = absint( $page ) + 1; - - if ( ! $single ) { - - // first/prev - if ( $page > 1 ) { - $this->link_header( 'first', $this->get_paginated_url( 1 ) ); - $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) ); - } - - // next - if ( $next_page <= $total_pages ) { - $this->link_header( 'next', $this->get_paginated_url( $next_page ) ); - } - - // last - if ( $page != $total_pages ) - $this->link_header( 'last', $this->get_paginated_url( $total_pages ) ); - } - - $this->header( 'X-WC-Total', $total ); - $this->header( 'X-WC-TotalPages', $total_pages ); - - do_action( 'woocommerce_api_pagination_headers', $this, $query ); - } - - /** - * Returns the request URL with the page query parameter set to the specified page - * - * @since 2.1 - * @param int $page - * @return string - */ - private function get_paginated_url( $page ) { - - // remove existing page query param - $request = remove_query_arg( 'page' ); - - // add provided page query param - $request = urldecode( add_query_arg( 'page', $page, $request ) ); - - // get the home host - $host = parse_url( get_home_url(), PHP_URL_HOST ); - - return set_url_scheme( "http://{$host}{$request}" ); - } - - /** - * Retrieve the raw request entity (body) - * - * @since 2.1 - * @return string - */ - public function get_raw_data() { - global $HTTP_RAW_POST_DATA; - - // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, - // but we can do it ourself. - if ( !isset( $HTTP_RAW_POST_DATA ) ) { - $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); - } - - return $HTTP_RAW_POST_DATA; - } - - /** - * Parse an RFC3339 datetime into a MySQl datetime - * - * Invalid dates default to unix epoch - * - * @since 2.1 - * @param string $datetime RFC3339 datetime - * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) - */ - public function parse_datetime( $datetime ) { - - // Strip millisecond precision (a full stop followed by one or more digits) - if ( strpos( $datetime, '.' ) !== false ) { - $datetime = preg_replace( '/\.\d+/', '', $datetime ); - } - - // default timezone to UTC - $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); - - try { - - $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); - - } catch ( Exception $e ) { - - $datetime = new DateTime( '@0' ); - - } - - return $datetime->format( 'Y-m-d H:i:s' ); - } - - /** - * Format a unix timestamp or MySQL datetime into an RFC3339 datetime - * - * @since 2.1 - * @param int|string $timestamp unix timestamp or MySQL datetime - * @param bool $convert_to_utc - * @return string RFC3339 datetime - */ - public function format_datetime( $timestamp, $convert_to_utc = false ) { - - if ( $convert_to_utc ) { - $timezone = new DateTimeZone( wc_timezone_string() ); - } else { - $timezone = new DateTimeZone( 'UTC' ); - } - - try { - - if ( is_numeric( $timestamp ) ) { - $date = new DateTime( "@{$timestamp}" ); - } else { - $date = new DateTime( $timestamp, $timezone ); - } - - // convert to UTC by adjusting the time based on the offset of the site's timezone - if ( $convert_to_utc ) { - $date->modify( -1 * $date->getOffset() . ' seconds' ); - } - - } catch ( Exception $e ) { - - $date = new DateTime( '@0' ); - } - - return $date->format( 'Y-m-d\TH:i:s\Z' ); - } - - /** - * Extract headers from a PHP-style $_SERVER array - * - * @since 2.1 - * @param array $server Associative array similar to $_SERVER - * @return array Headers extracted from the input - */ - public function get_headers($server) { - $headers = array(); - // CONTENT_* headers are not prefixed with HTTP_ - $additional = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true); - - foreach ($server as $key => $value) { - if ( strpos( $key, 'HTTP_' ) === 0) { - $headers[ substr( $key, 5 ) ] = $value; - } - elseif ( isset( $additional[ $key ] ) ) { - $headers[ $key ] = $value; - } - } - - return $headers; - } - - /** - * Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or - * the HTTP ACCEPT header - * - * @since 2.1 - * @return bool - */ - private function is_json_request() { - - // check path - if ( false !== stripos( $this->path, '.json' ) ) - return true; - - // check ACCEPT header, only 'application/json' is acceptable, see RFC 4627 - if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] ) - return true; - - return false; - } - - /** - * Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or - * the HTTP ACCEPT header - * - * @since 2.1 - * @return bool - */ - private function is_xml_request() { - - // check path - if ( false !== stripos( $this->path, '.xml' ) ) - return true; - - // check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376 - if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) ) - return true; - - return false; - } -} diff --git a/includes/api/class-wc-api-xml-handler.php b/includes/api/class-wc-api-xml-handler.php deleted file mode 100644 index b16b834a1d8..00000000000 --- a/includes/api/class-wc-api-xml-handler.php +++ /dev/null @@ -1,309 +0,0 @@ -xml = new XMLWriter(); - - $this->xml->openMemory(); - - $this->xml->setIndent(true); - - $this->xml->startDocument( '1.0', 'UTF-8' ); - - $root_element = key( $data ); - - $data = $data[ $root_element ]; - - switch ( $root_element ) { - - case 'orders': - $data = array( 'order' => $data ); - break; - - case 'order_notes': - $data = array( 'order_note' => $data ); - break; - - case 'customers': - $data = array( 'customer' => $data ); - break; - - case 'coupons': - $data = array( 'coupon' => $data ); - break; - - case 'products': - $data = array( 'product' => $data ); - break; - - case 'product_reviews': - $data = array( 'product_review' => $data ); - break; - - default: - $data = apply_filters( 'woocommerce_api_xml_data', $data, $root_element ); - break; - } - - // generate xml starting with the root element and recursively generating child elements - $this->array_to_xml( $root_element, $data ); - - $this->xml->endDocument(); - - return $this->xml->outputMemory(); - } - - /** - * Convert array into XML by recursively generating child elements - * - * @since 2.1 - * @param string|array $element_key - name for element, e.g. - * @param string|array $element_value - value for element, e.g. 1234 - * @return string - generated XML - */ - private function array_to_xml( $element_key, $element_value = array() ) { - - if ( is_array( $element_value ) ) { - - // handle attributes - if ( '@attributes' === $element_key ) { - foreach ( $element_value as $attribute_key => $attribute_value ) { - - $this->xml->startAttribute( $attribute_key ); - $this->xml->text( $attribute_value ); - $this->xml->endAttribute(); - } - return; - } - - // handle multi-elements (e.g. multiple elements) - if ( is_numeric( key( $element_value ) ) ) { - - // recursively generate child elements - foreach ( $element_value as $child_element_key => $child_element_value ) { - - $this->xml->startElement( $element_key ); - - foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) { - $this->array_to_xml( $sibling_element_key, $sibling_element_value ); - } - - $this->xml->endElement(); - } - - } else { - - // start root element - $this->xml->startElement( $element_key ); - - // recursively generate child elements - foreach ( $element_value as $child_element_key => $child_element_value ) { - $this->array_to_xml( $child_element_key, $child_element_value ); - } - - // end root element - $this->xml->endElement(); - } - - } else { - - // handle single elements - if ( '@value' == $element_key ) { - - $this->xml->text( $element_value ); - - } else { - - // wrap element in CDATA tags if it contains illegal characters - if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) { - - $this->xml->startElement( $element_key ); - $this->xml->writeCdata( $element_value ); - $this->xml->endElement(); - - } else { - - $this->xml->writeElement( $element_key, $element_value ); - } - - } - - return; - } - } - - /** - * Adjust the sales report array format to change totals keyed with the sales date to become an - * attribute for the totals element instead - * - * @since 2.1 - * @param array $data - * @return array - */ - public function format_sales_report_data( $data ) { - - if ( ! empty( $data['totals'] ) ) { - - foreach ( $data['totals'] as $date => $totals ) { - - unset( $data['totals'][ $date ] ); - - $data['totals'][] = array_merge( array( '@attributes' => array( 'date' => $date ) ), $totals ); - } - } - - return $data; - } - - /** - * Adjust the product data to handle options for attributes without a named child element and other - * fields that have no named child elements (e.g. categories = array( 'cat1', 'cat2' ) ) - * - * Note that the parent product data for variations is also adjusted in the same manner as needed - * - * @since 2.1 - * @param array $data - * @return array - */ - public function format_product_data( $data ) { - - // handle attribute values - if ( ! empty( $data['attributes'] ) ) { - - foreach ( $data['attributes'] as $attribute_key => $attribute ) { - - if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) { - - foreach ( $attribute['options'] as $option_key => $option ) { - - unset( $data['attributes'][ $attribute_key ]['options'][ $option_key ] ); - - $data['attributes'][ $attribute_key ]['options']['option'][] = array( $option ); - } - } - } - } - - // simple arrays are fine for JSON, but XML requires a child element name, so this adjusts the data - // array to define a child element name for each field - $fields_to_fix = array( - 'related_ids' => 'related_id', - 'upsell_ids' => 'upsell_id', - 'cross_sell_ids' => 'cross_sell_id', - 'categories' => 'category', - 'tags' => 'tag' - ); - - foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) { - - if ( ! empty( $data[ $parent_field_name ] ) ) { - - foreach ( $data[ $parent_field_name ] as $field_key => $field ) { - - unset( $data[ $parent_field_name ][ $field_key ] ); - - $data[ $parent_field_name ][ $child_field_name ][] = array( $field ); - } - } - } - - // handle adjusting the parent product for variations - if ( ! empty( $data['parent'] ) ) { - - // attributes - if ( ! empty( $data['parent']['attributes'] ) ) { - - foreach ( $data['parent']['attributes'] as $attribute_key => $attribute ) { - - if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) { - - foreach ( $attribute['options'] as $option_key => $option ) { - - unset( $data['parent']['attributes'][ $attribute_key ]['options'][ $option_key ] ); - - $data['parent']['attributes'][ $attribute_key ]['options']['option'][] = array( $option ); - } - } - } - } - - // fields - foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) { - - if ( ! empty( $data['parent'][ $parent_field_name ] ) ) { - - foreach ( $data['parent'][ $parent_field_name ] as $field_key => $field ) { - - unset( $data['parent'][ $parent_field_name ][ $field_key ] ); - - $data['parent'][ $parent_field_name ][ $child_field_name ][] = array( $field ); - } - } - } - } - - return $data; - } - -} diff --git a/includes/api/class-wc-rest-authentication.php b/includes/api/class-wc-rest-authentication.php new file mode 100644 index 00000000000..138fbd68bb8 --- /dev/null +++ b/includes/api/class-wc-rest-authentication.php @@ -0,0 +1,600 @@ +is_request_to_rest_api() ) { + return $user_id; + } + + if ( is_ssl() ) { + return $this->perform_basic_authentication(); + } else { + return $this->perform_oauth_authentication(); + } + } + + /** + * Check for authentication error. + * + * @param WP_Error|null|bool $error + * @return WP_Error|null|bool + */ + public function check_authentication_error( $error ) { + // Passthrough other errors. + if ( ! empty( $error ) ) { + return $error; + } + + return $this->get_error(); + } + + /** + * Set authentication error. + * + * @param WP_Error $error Authication error data. + */ + protected function set_error( $error ) { + // Reset user. + $this->user = null; + + $this->error = $error; + } + + /** + * Get authentication error. + * + * @return WP_Error|null. + */ + protected function get_error() { + return $this->error; + } + + /** + * Basic Authentication. + * + * SSL-encrypted requests are not subject to sniffing or man-in-the-middle + * attacks, so the request can be authenticated by simply looking up the user + * associated with the given consumer key and confirming the consumer secret + * provided is valid. + * + * @return int|bool + */ + private function perform_basic_authentication() { + $this->auth_method = 'basic_auth'; + $consumer_key = ''; + $consumer_secret = ''; + + // If the $_GET parameters are present, use those first. + if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) { + $consumer_key = $_GET['consumer_key']; + $consumer_secret = $_GET['consumer_secret']; + } + + // If the above is not present, we will do full basic auth. + if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { + $consumer_key = $_SERVER['PHP_AUTH_USER']; + $consumer_secret = $_SERVER['PHP_AUTH_PW']; + } + + // Stop if don't have any key. + if ( ! $consumer_key || ! $consumer_secret ) { + return false; + } + + // Get user data. + $this->user = $this->get_user_data_by_consumer_key( $consumer_key ); + if ( empty( $this->user ) ) { + return false; + } + + // Validate user secret. + if ( ! hash_equals( $this->user->consumer_secret, $consumer_secret ) ) { + $this->set_error( new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ) ); + + return false; + } + + return $this->user->user_id; + } + + /** + * Parse the Authorization header into parameters. + * + * @since 3.0.0 + * + * @param string $header Authorization header value (not including "Authorization: " prefix). + * + * @return array Map of parameter values. + */ + public function parse_header( $header ) { + if ( 'OAuth ' !== substr( $header, 0, 6 ) ) { + return array(); + } + + // From OAuth PHP library, used under MIT license. + $params = array(); + if ( preg_match_all( '/(oauth_[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches ) ) { + foreach ( $matches[1] as $i => $h ) { + $params[ $h ] = urldecode( empty( $matches[3][ $i ] ) ? $matches[4][ $i ] : $matches[3][ $i ] ); + } + if ( isset( $params['realm'] ) ) { + unset( $params['realm'] ); + } + } + + return $params; + } + + /** + * Get the authorization header. + * + * On certain systems and configurations, the Authorization header will be + * stripped out by the server or PHP. Typically this is then used to + * generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use + * `getallheaders` here to try and grab it out instead. + * + * @since 3.0.0 + * + * @return string Authorization header if set. + */ + public function get_authorization_header() { + if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) { + return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); + } + + if ( function_exists( 'getallheaders' ) ) { + $headers = getallheaders(); + // Check for the authoization header case-insensitively. + foreach ( $headers as $key => $value ) { + if ( 'authorization' === strtolower( $key ) ) { + return $value; + } + } + } + + return ''; + } + + /** + * Get oAuth parameters from $_GET, $_POST or request header. + * + * @since 3.0.0 + * + * @return array|WP_Error + */ + public function get_oauth_parameters() { + $params = array_merge( $_GET, $_POST ); + $params = wp_unslash( $params ); + $header = $this->get_authorization_header(); + + if ( ! empty( $header ) ) { + // Trim leading spaces. + $header = trim( $header ); + $header_params = $this->parse_header( $header ); + + if ( ! empty( $header_params ) ) { + $params = array_merge( $params, $header_params ); + } + } + + $param_names = array( + 'oauth_consumer_key', + 'oauth_timestamp', + 'oauth_nonce', + 'oauth_signature', + 'oauth_signature_method', + ); + + $errors = array(); + $have_one = false; + + // Check for required OAuth parameters. + foreach ( $param_names as $param_name ) { + if ( empty( $params[ $param_name ] ) ) { + $errors[] = $param_name; + } else { + $have_one = true; + } + } + + // All keys are missing, so we're probably not even trying to use OAuth. + if ( ! $have_one ) { + return array(); + } + + // If we have at least one supplied piece of data, and we have an error, + // then it's a failed authentication. + if ( ! empty( $errors ) ) { + $message = sprintf( + _n( 'Missing OAuth parameter %s', 'Missing OAuth parameters %s', count( $errors ), 'woocommerce' ), + implode( ', ', $errors ) + ); + + $this->set_error( new WP_Error( 'woocommerce_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ) ); + + return array(); + } + + return $params; + } + + /** + * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests. + * + * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP. + * + * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: + * + * 1) There is no token associated with request/responses, only consumer keys/secrets are used. + * + * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, + * This is because there is no cross-OS function within PHP to get the raw Authorization header. + * + * @link http://tools.ietf.org/html/rfc5849 for the full spec. + * + * @return int|bool + */ + private function perform_oauth_authentication() { + $this->auth_method = 'oauth1'; + + $params = $this->get_oauth_parameters(); + if ( empty( $params ) ) { + return false; + } + + // Fetch WP user by consumer key. + $this->user = $this->get_user_data_by_consumer_key( $params['oauth_consumer_key'] ); + + if ( empty( $this->user ) ) { + $this->set_error( new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ) ); + + return false; + } + + // Perform OAuth validation. + $signature = $this->check_oauth_signature( $this->user, $params ); + if ( is_wp_error( $signature ) ) { + $this->set_error( $signature ); + return false; + } + + $timestamp_and_nonce = $this->check_oauth_timestamp_and_nonce( $this->user, $params['oauth_timestamp'], $params['oauth_nonce'] ); + if ( is_wp_error( $timestamp_and_nonce ) ) { + $this->set_error( $timestamp_and_nonce ); + return false; + } + + return $this->user->user_id; + } + + /** + * Verify that the consumer-provided request signature matches our generated signature, + * this ensures the consumer has a valid key/secret. + * + * @param stdClass $user + * @param array $params The request parameters. + * @return true|WP_Error + */ + private function check_oauth_signature( $user, $params ) { + $http_method = strtoupper( $_SERVER['REQUEST_METHOD'] ); + $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); + $wp_base = get_home_url( null, '/', 'relative' ); + if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) { + $request_path = substr( $request_path, strlen( $wp_base ) ); + } + $base_request_uri = rawurlencode( get_home_url( null, $request_path ) ); + + // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature. + $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); + unset( $params['oauth_signature'] ); + + // Sort parameters. + if ( ! uksort( $params, 'strcmp' ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + // Normalize parameter key/values. + $params = $this->normalize_parameters( $params ); + $query_string = implode( '%26', $this->join_with_equals_sign( $params ) ); // Join with ampersand. + $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; + + if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); + $secret = $user->consumer_secret . '&'; + $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); + + if ( ! hash_equals( $signature, $consumer_signature ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + return true; + } + + /** + * Creates an array of urlencoded strings out of each array key/value pairs. + * + * @param array $params Array of parameters to convert. + * @param array $query_params Array to extend. + * @param string $key Optional Array key to append + * @return string Array of urlencoded strings + */ + private function join_with_equals_sign( $params, $query_params = array(), $key = '' ) { + foreach ( $params as $param_key => $param_value ) { + if ( $key ) { + $param_key = $key . '%5B' . $param_key . '%5D'; // Handle multi-dimensional array. + } + + if ( is_array( $param_value ) ) { + $query_params = $this->join_with_equals_sign( $param_value, $query_params, $param_key ); + } else { + $string = $param_key . '=' . $param_value; // Join with equals sign. + $query_params[] = wc_rest_urlencode_rfc3986( $string ); + } + } + + return $query_params; + } + + /** + * Normalize each parameter by assuming each parameter may have already been + * encoded, so attempt to decode, and then re-encode according to RFC 3986. + * + * Note both the key and value is normalized so a filter param like: + * + * 'filter[period]' => 'week' + * + * is encoded to: + * + * 'filter%255Bperiod%255D' => 'week' + * + * This conforms to the OAuth 1.0a spec which indicates the entire query string + * should be URL encoded. + * + * @see rawurlencode() + * @param array $parameters Un-normalized parameters. + * @return array Normalized parameters. + */ + private function normalize_parameters( $parameters ) { + $keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) ); + $values = wc_rest_urlencode_rfc3986( array_values( $parameters ) ); + $parameters = array_combine( $keys, $values ); + + return $parameters; + } + + /** + * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where + * an attacker could attempt to re-send an intercepted request at a later time. + * + * - A timestamp is valid if it is within 15 minutes of now. + * - A nonce is valid if it has not been used within the last 15 minutes. + * + * @param stdClass $user + * @param int $timestamp the unix timestamp for when the request was made + * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated + * @return bool|WP_Error + */ + private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) { + global $wpdb; + + $valid_window = 15 * 60; // 15 minute window. + + if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $used_nonces = maybe_unserialize( $user->nonces ); + + if ( empty( $used_nonces ) ) { + $used_nonces = array(); + } + + if ( in_array( $nonce, $used_nonces ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $used_nonces[ $timestamp ] = $nonce; + + // Remove expired nonces. + foreach ( $used_nonces as $nonce_timestamp => $nonce ) { + if ( $nonce_timestamp < ( time() - $valid_window ) ) { + unset( $used_nonces[ $nonce_timestamp ] ); + } + } + + $used_nonces = maybe_serialize( $used_nonces ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'nonces' => $used_nonces ), + array( 'key_id' => $user->key_id ), + array( '%s' ), + array( '%d' ) + ); + + return true; + } + + /** + * Return the user data for the given consumer_key. + * + * @param string $consumer_key + * @return array + */ + private function get_user_data_by_consumer_key( $consumer_key ) { + global $wpdb; + + $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); + $user = $wpdb->get_row( $wpdb->prepare( " + SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces + FROM {$wpdb->prefix}woocommerce_api_keys + WHERE consumer_key = %s + ", $consumer_key ) ); + + return $user; + } + + /** + * Check that the API keys provided have the proper key-specific permissions to either read or write API resources. + * + * @param string $method Request method. + * @return bool|WP_Error + */ + private function check_permissions( $method ) { + $permissions = $this->user->permissions; + + switch ( $method ) { + case 'HEAD' : + case 'GET' : + if ( 'read' !== $permissions && 'read_write' !== $permissions ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'woocommerce' ), array( 'status' => 401 ) ); + } + break; + case 'POST' : + case 'PUT' : + case 'PATCH' : + case 'DELETE' : + if ( 'write' !== $permissions && 'read_write' !== $permissions ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'woocommerce' ), array( 'status' => 401 ) ); + } + break; + + default : + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Unknown request method.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + return true; + } + + /** + * Updated API Key last access datetime. + */ + private function update_last_access() { + global $wpdb; + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'last_access' => current_time( 'mysql' ) ), + array( 'key_id' => $this->user->key_id ), + array( '%s' ), + array( '%d' ) + ); + } + + /** + * If the consumer_key and consumer_secret $_GET parameters are NOT provided + * and the Basic auth headers are either not present or the consumer secret does not match the consumer + * key provided, then return the correct Basic headers and an error message. + * + * @param WP_REST_Response $response Current response being served. + * @return WP_REST_Response + */ + public function send_unauthorized_headers( $response ) { + if ( is_wp_error( $this->get_error() ) && 'basic_auth' === $this->auth_method ) { + $auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' ); + $response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true ); + } + + return $response; + } + + /** + * Check for user permissions and register last access. + * + * @param mixed $result Response to replace the requested version with. + * @param WP_REST_Server $server Server instance. + * @param WP_REST_Request $request Request used to generate the response. + * @return mixed + */ + public function check_user_permissions( $result, $server, $request ) { + if ( $this->user ) { + // Check API Key permissions. + $allowed = $this->check_permissions( $request->get_method() ); + if ( is_wp_error( $allowed ) ) { + return $allowed; + } + + // Register last access. + $this->update_last_access(); + } + + return $result; + } +} + +new WC_REST_Authentication(); diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php new file mode 100644 index 00000000000..796d0c02536 --- /dev/null +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -0,0 +1,538 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return new WC_Coupon( $id ); + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data instance. + * @return array + */ + protected function get_formatted_item_data( $object ) { + $data = $object->get_data(); + + $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); + $format_date = array( 'date_created', 'date_modified', 'date_expires' ); + $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format null values. + foreach ( $format_null as $key ) { + $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; + } + + return array( + 'id' => $object->get_id(), + 'code' => $data['code'], + 'amount' => $data['amount'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'discount_type' => $data['discount_type'], + 'description' => $data['description'], + 'date_expires' => $data['date_expires'], + 'date_expires_gmt' => $data['date_expires_gmt'], + 'usage_count' => $data['usage_count'], + 'individual_use' => $data['individual_use'], + 'product_ids' => $data['product_ids'], + 'excluded_product_ids' => $data['excluded_product_ids'], + 'usage_limit' => $data['usage_limit'], + 'usage_limit_per_user' => $data['usage_limit_per_user'], + 'limit_usage_to_x_items' => $data['limit_usage_to_x_items'], + 'free_shipping' => $data['free_shipping'], + 'product_categories' => $data['product_categories'], + 'excluded_product_categories' => $data['excluded_product_categories'], + 'exclude_sale_items' => $data['exclude_sale_items'], + 'minimum_amount' => $data['minimum_amount'], + 'maximum_amount' => $data['maximum_amount'], + 'email_restrictions' => $data['email_restrictions'], + 'used_by' => $data['used_by'], + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Prepare a single coupon output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = $this->get_formatted_item_data( $object ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + if ( ! empty( $request['code'] ) ) { + $id = wc_get_coupon_id_by_code( $request['code'] ); + $args['post__in'] = array( $id ); + } + + // Get only ids. + $args['fields'] = 'ids'; + + return $args; + } + + /** + * Only reutrn writeable props from schema. + * + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single coupon for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $coupon = new WC_Coupon( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Validate required POST fields. + if ( $creating && empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'code' : + $coupon_code = wc_format_coupon_code( $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon->set_code( $coupon_code ); + break; + case 'meta_data' : + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + case 'description' : + $coupon->set_description( wp_filter_post_kses( $value ) ); + break; + default : + if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { + $coupon->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating ); + } + + /** + * Get the Coupon's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'amount' => array( + 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the coupon was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the coupon was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'fixed_cart', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_expires' => array( + 'description' => __( "The date the coupon expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_expires_gmt' => array( + 'description' => __( "The date the coupon expires, as GMT.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'individual_use' => array( + 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( "List of product IDs the coupon can be used on.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_ids' => array( + 'description' => __( "List of product IDs the coupon cannot be used on.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'limit_usage_to_x_items' => array( + 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'free_shipping' => array( + 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_categories' => array( + 'description' => __( "List of category IDs the coupon applies to.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_categories' => array( + 'description' => __( "List of category IDs the coupon does not apply to.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email_restrictions' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + 'used_by' => array( + 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['code'] = array( + 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-customer-downloads-controller.php b/includes/api/class-wc-rest-customer-downloads-controller.php new file mode 100644 index 00000000000..9b39be99ea3 --- /dev/null +++ b/includes/api/class-wc-rest-customer-downloads-controller.php @@ -0,0 +1,169 @@ +/downloads endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Customers controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Customer_Downloads_V1_Controller + */ +class WC_REST_Customer_Downloads_Controller extends WC_REST_Customer_Downloads_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Prepare a single download output for response. + * + * @param stdClass $download Download object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $download, $request ) { + $data = array( + 'download_id' => $download->download_id, + 'download_url' => $download->download_url, + 'product_id' => $download->product_id, + 'product_name' => $download->product_name, + 'download_name' => $download->download_name, + 'order_id' => $download->order_id, + 'order_key' => $download->order_key, + 'downloads_remaining' => '' === $download->downloads_remaining ? 'unlimited' : $download->downloads_remaining, + 'access_expires' => $download->access_expires ? wc_rest_prepare_date_response( $download->access_expires ) : 'never', + 'access_expires_gmt' => $download->access_expires ? wc_rest_prepare_date_response( get_gmt_from_date( $download->access_expires ) ) : 'never', + 'file' => $download->file, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $download, $request ) ); + + /** + * Filter customer download data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $download Download object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request ); + } + + /** + * Get the Customer Download's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer_download', + 'type' => 'object', + 'properties' => array( + 'download_id' => array( + 'description' => __( 'Download ID (MD5).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_url' => array( + 'description' => __( 'Download file URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Downloadable product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_name' => array( + 'description' => __( 'Downloadable file name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_id' => array( + 'description' => __( 'Order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'downloads_remaining' => array( + 'description' => __( 'Number of downloads remaining.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires' => array( + 'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires_gmt' => array( + 'description' => __( "The date when download access expires, as GMT.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File details.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php new file mode 100644 index 00000000000..09bae50aaa6 --- /dev/null +++ b/includes/api/class-wc-rest-customers-controller.php @@ -0,0 +1,368 @@ +get_data(); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + return array( + 'id' => $object->get_id(), + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'role' => $data['role'], + 'username' => $data['username'], + 'billing' => $data['billing'], + 'shipping' => $data['shipping'], + 'is_paying_customer' => $data['is_paying_customer'], + 'orders_count' => $object->get_order_count(), + 'total_spent' => $object->get_total_spent(), + 'avatar_url' => $object->get_avatar_url(), + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $user_data User object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $user_data, $request ) { + $customer = new WC_Customer( $user_data->ID ); + $data = $this->get_formatted_item_data( $customer ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $user_data ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $user_data User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); + } + + /** + * Update customer meta fields. + * + * @param WC_Customer $customer + * @param WP_REST_Request $request + */ + protected function update_customer_meta_fields( $customer, $request ) { + parent::update_customer_meta_fields( $customer, $request ); + + // Meta data. + if ( isset( $request['meta_data'] ) ) { + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $customer->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + } + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the order was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'role' => array( + 'description' => __( 'Customer role.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'is_paying_customer' => array( + 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), + 'type' => 'bool', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'orders_count' => array( + 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_spent' => array( + 'description' => __( 'Total amount spent.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-exception.php b/includes/api/class-wc-rest-exception.php new file mode 100644 index 00000000000..4d575a225de --- /dev/null +++ b/includes/api/class-wc-rest-exception.php @@ -0,0 +1,20 @@ +/notes endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Notes controller class. + * + * @package WooCommerce/API + * @extends WC_REST_ControllerWC_REST_Order_Notes_V1_Controller + */ +class WC_REST_Order_Notes_Controller extends WC_REST_Order_Notes_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Get order notes from an order. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $args = array( + 'post_id' => $order->get_id(), + 'approve' => 'approve', + 'type' => 'order_note', + ); + + // Allow filter by order note type. + if ( 'customer' === $request['type'] ) { + $args['meta_query'] = array( + array( + 'key' => 'is_customer_note', + 'value' => 1, + 'compare' => '=', + ), + ); + } elseif ( 'internal' === $request['type'] ) { + $args['meta_query'] = array( + array( + 'key' => 'is_customer_note', + 'compare' => 'NOT EXISTS', + ), + ); + } + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $data = array(); + foreach ( $notes as $note ) { + $order_note = $this->prepare_item_for_response( $note, $request ); + $order_note = $this->prepare_response_for_collection( $order_note ); + $data[] = $order_note; + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a single order note output for response. + * + * @param WP_Comment $note Order note object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $note, $request ) { + $data = array( + 'id' => (int) $note->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $note->comment_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $note ) ); + + /** + * Filter order note object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $note Order note object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); + } + + /** + * Get the Order Notes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'order_note', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( "The date the order note was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'note' => array( + 'description' => __( 'Order note content.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_note' => array( + 'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + $params['type'] = array( + 'default' => 'any', + 'description' => __( 'Limit result to customers or internal notes.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'any', 'customer', 'internal' ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php new file mode 100644 index 00000000000..8db3beaf204 --- /dev/null +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -0,0 +1,553 @@ +/refunds endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Orders_Controller + */ +class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/refunds'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order_refund'; + + /** + * Stores the request. + * @var array + */ + protected $request = array(); + + /** + * Order refunds actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_object_trashable", '__return_false' ); + } + + /** + * Register the routes for order refunds. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => true, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_order( $id ); + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data instance. + * @return array + */ + protected function get_formatted_item_data( $object ) { + $data = $object->get_data(); + $format_decimal = array( 'amount' ); + $format_date = array( 'date_created' ); + $format_line_items = array( 'line_items' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format line items. + foreach ( $format_line_items as $key ) { + $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); + } + + return array( + 'id' => $object->get_id(), + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'amount' => $data['amount'], + 'reason' => $data['reason'], + 'refunded_by' => $data['refunded_by'], + 'meta_data' => $data['meta_data'], + 'line_items' => $data['line_items'], + ); + } + + /** + * Prepare a single order output for response. + * + * @since 3.0.0 + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $this->request = $request; + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + if ( ! $object || $object->get_parent_id() !== $order->get_id() ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $data = $this->get_formatted_item_data( $object ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $base = str_replace( '(?P[\d]+)', $object->get_parent_id(), $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), + ), + ); + + return $links; + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + $args['post_status'] = array_keys( wc_get_order_statuses() ); + $args['post_parent__in'] = array( absint( $request['order_id'] ) ); + + return $args; + } + + /** + * Prepares one object for create or update operation. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( array( + 'order_id' => $order->get_id(), + 'amount' => $request['amount'], + 'reason' => empty( $request['reason'] ) ? null : $request['reason'], + 'line_items' => $request['line_items'], + 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, + 'restock_items' => true, + ) ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( "The date the order refund was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'Refund amount.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reason' => array( + 'description' => __( 'Reason for refund.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'refunded_by' => array( + 'description' => __( 'User ID of user who created the refund.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'api_refund' => array( + 'description' => __( 'When true, the payment gateway API is used to generate the refund.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + 'default' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + unset( $params['status'], $params['customer'], $params['product'] ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php new file mode 100644 index 00000000000..15f6cdae9b5 --- /dev/null +++ b/includes/api/class-wc-rest-orders-controller.php @@ -0,0 +1,1659 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_order( $id ); + } + + /** + * Expands an order item to get its data. + * @param WC_Order_item $item + * @return array + */ + protected function get_order_item_data( $item ) { + $data = $item->get_data(); + $format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + if ( isset( $data[ $key ] ) ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + } + + // Add SKU and PRICE to products. + if ( is_callable( array( $item, 'get_product' ) ) ) { + $data['sku'] = $item->get_product() ? $item->get_product()->get_sku(): null; + $data['price'] = $item->get_total() / max( 1, $item->get_quantity() ); + } + + // Format taxes. + if ( ! empty( $data['taxes']['total'] ) ) { + $taxes = array(); + + foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) { + $taxes[] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '', + ); + } + $data['taxes'] = $taxes; + } elseif ( isset( $data['taxes'] ) ) { + $data['taxes'] = array(); + } + + // Remove names for coupons, taxes and shipping. + if ( isset( $data['code'] ) || isset( $data['rate_code'] ) || isset( $data['method_title'] ) ) { + unset( $data['name'] ); + } + + // Remove props we don't want to expose. + unset( $data['order_id'] ); + unset( $data['type'] ); + + return $data; + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data instance. + * @return array + */ + protected function get_formatted_item_data( $object ) { + $data = $object->get_data(); + $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); + $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); + $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format the order status. + $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; + + // Format line items. + foreach ( $format_line_items as $key ) { + $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); + } + + // Refunds. + $data['refunds'] = array(); + foreach ( $object->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'refund' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ), + ); + } + + return array( + 'id' => $object->get_id(), + 'parent_id' => $data['parent_id'], + 'number' => $data['number'], + 'order_key' => $data['order_key'], + 'created_via' => $data['created_via'], + 'version' => $data['version'], + 'status' => $data['status'], + 'currency' => $data['currency'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'discount_total' => $data['discount_total'], + 'discount_tax' => $data['discount_tax'], + 'shipping_total' => $data['shipping_total'], + 'shipping_tax' => $data['shipping_tax'], + 'cart_tax' => $data['cart_tax'], + 'total' => $data['total'], + 'total_tax' => $data['total_tax'], + 'prices_include_tax' => $data['prices_include_tax'], + 'customer_id' => $data['customer_id'], + 'customer_ip_address' => $data['customer_ip_address'], + 'customer_user_agent' => $data['customer_user_agent'], + 'customer_note' => $data['customer_note'], + 'billing' => $data['billing'], + 'shipping' => $data['shipping'], + 'payment_method' => $data['payment_method'], + 'payment_method_title' => $data['payment_method_title'], + 'transaction_id' => $data['transaction_id'], + 'date_paid' => $data['date_paid'], + 'date_paid_gmt' => $data['date_paid_gmt'], + 'date_completed' => $data['date_completed'], + 'date_completed_gmt' => $data['date_completed_gmt'], + 'cart_hash' => $data['cart_hash'], + 'meta_data' => $data['meta_data'], + 'line_items' => $data['line_items'], + 'tax_lines' => $data['tax_lines'], + 'shipping_lines' => $data['shipping_lines'], + 'fee_lines' => $data['fee_lines'], + 'coupon_lines' => $data['coupon_lines'], + 'refunds' => $data['refunds'], + ); + } + + /** + * Prepare a single order output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $this->request = $request; + $data = $this->get_formatted_item_data( $object ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $object->get_customer_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ), + ); + } + + if ( 0 !== (int) $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + global $wpdb; + + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + if ( 'any' !== $request['status'] ) { + $args['post_status'] = 'wc-' . $request['status']; + } else { + $args['post_status'] = 'any'; + } + + if ( isset( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $request['product'] ) ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + return $args; + } + + /** + * Only reutrn writeable props from schema. + * + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single order for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'billing' : + case 'shipping' : + $this->update_address( $order, $value, $key ); + break; + case 'line_items' : + case 'shipping_lines' : + case 'fee_lines' : + case 'coupon_lines' : + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data' : + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default : + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $order Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + if ( $creating ) { + // Make sure customer exists. + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $object->set_created_via( 'rest-api' ); + $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $object->calculate_totals(); + } + + $object->save(); + + // Actions for after the order is saved. + if ( $creating ) { + if ( true === $request['set_paid'] ) { + $object->payment_complete( $request['transaction_id'] ); + } + } else { + // Handle set paid. + if ( $object->needs_payment() && true === $request['set_paid'] ) { + $object->payment_complete( $request['transaction_id'] ); + } + + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $object->calculate_totals(); + } + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @param array $posted Request data + * + * @return int + * @throws WC_REST_Exception + */ + protected function get_product_id( $posted ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * + * @param WC_Order_Item $item + * @param string $prop + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * + * @param WC_Order_Item $item + * @param string[] $props + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } + } + + /** + * Maybe set item meta if posted. + * + * @param WC_Order_Item $item + * @param array $posted Request data. + */ + protected function maybe_set_item_meta_data( $item, $posted ) { + if ( ! empty( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) { + foreach ( $posted['meta_data'] as $meta ) { + if ( isset( $meta['key'], $meta['value'] ) ) { + $item->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + } + } + + /** + * Create or update a line item. + * + * @param array $posted Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * + * @return WC_Order_Item_Product + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_line_items( $posted, $action = 'create' ) { + $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + $product = wc_get_product( $this->get_product_id( $posted ) ); + + if ( $product !== $item->get_product() ) { + $item->set_product( $product ); + + if ( 'create' === $action ) { + $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $item->set_total( $total ); + $item->set_subtotal( $total ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order shipping method. + * + * @param $posted $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * + * @return WC_Order_Item_Shipping + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_shipping_lines( $posted, $action ) { + $item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order fee. + * + * @param array $posted Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * + * @return WC_Order_Item_Fee + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_fee_lines( $posted, $action ) { + $item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order coupon. + * + * @param array $posted Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * + * @return WC_Order_Item_Coupon + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_coupon_lines( $posted, $action ) { + $item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order + * @param string $item_type + * @param array $posted item provided in the request body + * @throws WC_REST_Exception If item ID is not associated with order + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $posted['id'] ), + absint( $order->get_id() ) + ) ); + if ( is_null( $result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data + $item = $this->$method( $posted, $action ); + + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // Save or add to order + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Get order statuses without prefixes. + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'version' => array( + 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $this->get_order_statuses(), + 'context' => array( 'view', 'edit' ), + ), + 'currency' => array( + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'default' => get_woocommerce_currency(), + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( "The date the order was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'User agent of the customer.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'billing' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_paid' => array( + 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid_gmt' => array( + 'description' => __( "The date the order was paid, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_completed_gmt' => array( + 'description' => __( "The date the order was completed, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Shipping lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'fee_lines' => array( + 'description' => __( 'Fee lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'taxable', 'none' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Coupons line data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'refunds' => array( + 'description' => __( 'List of refunds.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Refund ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reason' => array( + 'description' => __( 'Refund reason.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Refund total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-payment-gateways-controller.php b/includes/api/class-wc-rest-payment-gateways-controller.php new file mode 100644 index 00000000000..8f46b38285a --- /dev/null +++ b/includes/api/class-wc-rest-payment-gateways-controller.php @@ -0,0 +1,461 @@ + + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to view payment gateways. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to read a payment gateway. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to edit payment gateways. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get payment gateways. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + $response = array(); + foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) { + $payment_gateway->id = $payment_gateway_id; + $gateway = $this->prepare_item_for_response( $payment_gateway, $request ); + $gateway = $this->prepare_response_for_collection( $gateway ); + $response[] = $gateway; + } + return rest_ensure_response( $response ); + } + + /** + * Get a single payment gateway. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $gateway = $this->get_gateway( $request ); + + if ( is_null( $gateway ) ) { + return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $gateway = $this->prepare_item_for_response( $gateway, $request ); + return rest_ensure_response( $gateway ); + } + + /** + * Update A Single Payment Method. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function update_item( $request ) { + $gateway = $this->get_gateway( $request ); + + if ( is_null( $gateway ) ) { + return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Get settings. + $gateway->init_form_fields(); + $settings = $gateway->settings; + + // Update settings. + if ( isset( $request['settings'] ) ) { + $errors_found = false; + foreach ( $gateway->form_fields as $key => $field ) { + if ( isset( $request['settings'][ $key ] ) ) { + if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) { + $value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field ); + } else { + $value = $this->validate_setting_text_field( $request['settings'][ $key ], $field ); + } + if ( is_wp_error( $value ) ) { + $errors_found = true; + break; + } + $settings[ $key ] = $value; + } + } + + if ( $errors_found ) { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update if this method is enabled or not. + if ( isset( $request['enabled'] ) ) { + $gateway->enabled = $settings['enabled'] = wc_bool_to_string( $request['enabled'] ); + } + + // Update title. + if ( isset( $request['title'] ) ) { + $gateway->title = $settings['title'] = $request['title']; + } + + // Update description. + if ( isset( $request['description'] ) ) { + $gateway->description = $settings['description'] = $request['description']; + } + + // Update options. + $gateway->settings = $settings; + update_option( $gateway->get_option_key(), apply_filters( 'woocommerce_gateway_' . $gateway->id . '_settings_values', $settings, $gateway ) ); + + // Update order + if ( isset( $request['order'] ) ) { + $order = (array) get_option( 'woocommerce_gateway_order' ); + $order[ $gateway->id ] = $request['order']; + update_option( 'woocommerce_gateway_order', $order ); + $gateway->order = absint( $request['order'] ); + } + + $gateway = $this->prepare_item_for_response( $gateway, $request ); + return rest_ensure_response( $gateway ); + } + + /** + * Get a gateway based on the current request object. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|null + */ + public function get_gateway( $request ) { + $gateway = null; + $payment_gateways = WC()->payment_gateways->payment_gateways(); + foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) { + if ( $request['id'] !== $payment_gateway_id ) { + continue; + } + $payment_gateway->id = $payment_gateway_id; + $gateway = $payment_gateway; + } + return $gateway; + } + + /** + * Prepare a payment gateway for response. + * + * @param WC_Payment_Gateway $gateway Payment gateway object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $gateway, $request ) { + $order = (array) get_option( 'woocommerce_gateway_order' ); + $item = array( + 'id' => $gateway->id, + 'title' => $gateway->title, + 'description' => $gateway->description, + 'order' => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '', + 'enabled' => ( 'yes' === $gateway->enabled ), + 'method_title' => $gateway->get_method_title(), + 'method_description' => $gateway->get_method_description(), + 'settings' => $this->get_settings( $gateway ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $gateway, $request ) ); + + /** + * Filter payment gateway objects returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Payment_Gateway $gateway Payment gateway object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request ); + } + + /** + * Return settings associated with this payment gateway. + * + * @param WC_Payment_Gateway $gateway + * + * @return array + */ + public function get_settings( $gateway ) { + $settings = array(); + $gateway->init_form_fields(); + foreach ( $gateway->form_fields as $id => $field ) { + // Make sure we at least have a title and type + if ( empty( $field['title'] ) || empty( $field['type'] ) ) { + continue; + } + // Ignore 'title' settings/fields -- they are UI only + if ( 'title' === $field['type'] ) { + continue; + } + // Ignore 'enabled' and 'description' which get included elsewhere. + if ( in_array( $id, array( 'enabled', 'description' ) ) ) { + continue; + } + $data = array( + 'id' => $id, + 'label' => empty( $field['label'] ) ? $field['title'] : $field['label'], + 'description' => empty( $field['description'] ) ? '' : $field['description'], + 'type' => $field['type'], + 'value' => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ], + 'default' => empty( $field['default'] ) ? '' : $field['default'], + 'tip' => empty( $field['description'] ) ? '' : $field['description'], + 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + return $settings; + } + + /** + * Prepare links for the request. + * + * @param WC_Payment_Gateway $gateway Payment gateway object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $gateway, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $gateway->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the payment gateway schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'payment_gateway', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Payment gateway ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'title' => array( + 'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'order' => array( + 'description' => __( 'Payment gateway sort order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'enabled' => array( + 'description' => __( 'Payment gateway enabled status.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'method_title' => array( + 'description' => __( 'Payment gateway method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_description' => array( + 'description' => __( 'Payment gateway method description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'settings' => array( + 'description' => __( 'Payment gateway settings.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Type of setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Setting value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'default' => array( + 'description' => __( 'Default value for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tip' => array( + 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'placeholder' => array( + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } + +} diff --git a/includes/api/class-wc-rest-product-attribute-terms-controller.php b/includes/api/class-wc-rest-product-attribute-terms-controller.php new file mode 100644 index 00000000000..27f52acdbe5 --- /dev/null +++ b/includes/api/class-wc-rest-product-attribute-terms-controller.php @@ -0,0 +1,31 @@ +/terms endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Attribute Terms controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Product_Attribute_Terms_V1_Controller + */ +class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Product_Attribute_Terms_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; +} diff --git a/includes/api/class-wc-rest-product-attributes-controller.php b/includes/api/class-wc-rest-product-attributes-controller.php new file mode 100644 index 00000000000..d8bb4410b5c --- /dev/null +++ b/includes/api/class-wc-rest-product-attributes-controller.php @@ -0,0 +1,31 @@ +term_id, 'display_type' ); + + // Get category order. + $menu_order = get_woocommerce_term_meta( $item->term_id, 'order' ); + + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'parent' => (int) $item->parent, + 'description' => $item->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => array(), + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + if ( $image_id = get_woocommerce_term_meta( $item->term_id, 'thumbnail_id' ) ) { + $attachment = get_post( $image_id ); + + $data['image'] = array( + 'id' => (int) $image_id, + 'date_created' => wc_rest_prepare_date_response( $attachment->post_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), + 'src' => wp_get_attachment_url( $image_id ), + 'title' => get_the_title( $attachment ), + 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Category schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'parent' => array( + 'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'display' => array( + 'description' => __( 'Category archive display type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'default', + 'enum' => array( 'default', 'products', 'subcategories', 'both' ), + 'context' => array( 'view', 'edit' ), + ), + 'image' => array( + 'description' => __( 'Image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-product-reviews-controller.php b/includes/api/class-wc-rest-product-reviews-controller.php new file mode 100644 index 00000000000..9c49a1d8568 --- /dev/null +++ b/includes/api/class-wc-rest-product-reviews-controller.php @@ -0,0 +1,201 @@ +/reviews. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Reviews Controller Class. + * + * @package WooCommerce/API + * @extends WC_REST_Product_Reviews_V1_Controller + */ +class WC_REST_Product_Reviews_Controller extends WC_REST_Product_Reviews_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + parent::register_routes(); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check if a given request has access to batch manage product reviews. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'product', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), + 'name' => $review->comment_author, + 'email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + + /** + * Bulk create, update and delete items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + $items = array_filter( $request->get_params() ); + $params = $request->get_url_params(); + $product_id = $params['product_id']; + $body_params = array(); + + foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { + if ( ! empty( $items[ $batch_type ] ) ) { + $injected_items = array(); + foreach ( $items[ $batch_type ] as $item ) { + $injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item; + } + $body_params[ $batch_type ] = $injected_items; + } + } + + $request = new WP_REST_Request( $request->get_method() ); + $request->set_body_params( $body_params ); + + return parent::batch_items( $request ); + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_created_gmt' => array( + 'description' => __( "The date the review was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-product-shipping-classes-controller.php b/includes/api/class-wc-rest-product-shipping-classes-controller.php new file mode 100644 index 00000000000..072ad524d30 --- /dev/null +++ b/includes/api/class-wc-rest-product-shipping-classes-controller.php @@ -0,0 +1,31 @@ +/variations endpoints. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API variations controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Products_Controller + */ +class WC_REST_Product_Variations_Controller extends WC_REST_Products_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/variations'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'product_variation'; + + /** + * Initialize product actions (parent). + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'add_product_id' ), 9, 2 ); + parent::__construct(); + } + + /** + * Register the routes for products. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_product( $id ); + } + + /** + * Prepare a single variation output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = array( + 'id' => $object->get_id(), + 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ), + 'description' => wc_format_content( $object->get_description() ), + 'permalink' => $object->get_permalink(), + 'sku' => $object->get_sku(), + 'price' => $object->get_price(), + 'regular_price' => $object->get_regular_price(), + 'sale_price' => $object->get_sale_price(), + 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ), + 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ), + 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ), + 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ), + 'on_sale' => $object->is_on_sale(), + 'visible' => $object->is_visible(), + 'purchasable' => $object->is_purchasable(), + 'virtual' => $object->is_virtual(), + 'downloadable' => $object->is_downloadable(), + 'downloads' => $this->get_downloads( $object ), + 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, + 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, + 'tax_status' => $object->get_tax_status(), + 'tax_class' => $object->get_tax_class(), + 'manage_stock' => $object->managing_stock(), + 'stock_quantity' => $object->get_stock_quantity(), + 'in_stock' => $object->is_in_stock(), + 'backorders' => $object->get_backorders(), + 'backorders_allowed' => $object->backorders_allowed(), + 'backordered' => $object->is_on_backorder(), + 'weight' => $object->get_weight(), + 'dimensions' => array( + 'length' => $object->get_length(), + 'width' => $object->get_width(), + 'height' => $object->get_height(), + ), + 'shipping_class' => $object->get_shipping_class(), + 'shipping_class_id' => $object->get_shipping_class_id(), + 'image' => current( $this->get_images( $object ) ), + 'attributes' => $this->get_attributes( $object ), + 'menu_order' => $object->get_menu_order(), + 'meta_data' => $object->get_meta_data(), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + $args['post_parent'] = $request['product_id']; + + return $args; + } + + /** + * Prepare a single variation for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + if ( isset( $request['id'] ) ) { + $variation = wc_get_product( absint( $request['id'] ) ); + } else { + $variation = new WC_Product_Variation(); + } + + $variation->set_parent_id( absint( $request['product_id'] ) ); + + // Status. + if ( isset( $request['visible'] ) ) { + $variation->set_status( false === $request['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $request['sku'] ) ) { + $variation->set_sku( wc_clean( $request['sku'] ) ); + } + + // Thumbnail. + if ( isset( $request['image'] ) ) { + if ( is_array( $request['image'] ) ) { + $image = $request['image']; + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->set_product_images( $variation, array( $image ) ); + } else { + $variation->set_image_id( '' ); + } + } + + // Virtual variation. + if ( isset( $request['virtual'] ) ) { + $variation->set_virtual( $request['virtual'] ); + } + + // Downloadable variation. + if ( isset( $request['downloadable'] ) ) { + $variation->set_downloadable( $request['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $variation->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $variation->set_download_expiry( $request['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $request ); + + // Stock handling. + if ( isset( $request['manage_stock'] ) ) { + $variation->set_manage_stock( $request['manage_stock'] ); + } + + if ( isset( $request['in_stock'] ) ) { + $variation->set_stock_status( true === $request['in_stock'] ? 'instock' : 'outofstock' ); + } + + if ( isset( $request['backorders'] ) ) { + $variation->set_backorders( $request['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $request['stock_quantity'] ) ) { + $variation->set_stock_quantity( $request['stock_quantity'] ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $variation->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $variation->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + + // Tax class. + if ( isset( $request['tax_class'] ) ) { + $variation->set_tax_class( $request['tax_class'] ); + } + + // Description. + if ( isset( $request['description'] ) ) { + $variation->set_description( wp_kses_post( $request['description'] ) ); + } + + // Update taxonomies. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + $parent = wc_get_product( $variation->get_parent_id() ); + $parent_attributes = $parent->get_attributes(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + // Menu order. + if ( $request['menu_order'] ) { + $variation->set_menu_order( $request['menu_order'] ); + } + + // Meta data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $variation Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); + } + + /** + * Clear caches here so in sync with any new variations. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_parent_id() ); + wp_cache_delete( 'product-' . $object->get_parent_id(), 'products' ); + } + + /** + * Delete a variation. + * + * @param WP_REST_Request $request Full details about the request + * + * @return bool|WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Bulk create, update and delete items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + $items = array_filter( $request->get_params() ); + $params = $request->get_url_params(); + $product_id = $params['product_id']; + $body_params = array(); + + foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { + if ( ! empty( $items[ $batch_type ] ) ) { + $injected_items = array(); + foreach ( $items[ $batch_type ] as $item ) { + $injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item; + } + $body_params[ $batch_type ] = $injected_items; + } + } + + $request = new WP_REST_Request( $request->get_method() ); + $request->set_body_params( $body_params ); + + return parent::batch_items( $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $product_id = (int) $request['product_id']; + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), + ), + ); + return $links; + } + + /** + * Get the Variation's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Variation description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Variation URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current variation price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Variation regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Variation sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'on_sale' => array( + 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'purchasable' => array( + 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the variation is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at variation level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Variation dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'image' => array( + 'description' => __( 'Variation image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php new file mode 100644 index 00000000000..ce0622abeac --- /dev/null +++ b/includes/api/class-wc-rest-products-controller.php @@ -0,0 +1,2079 @@ +post_type}_object", array( $this, 'clear_transients' ) ); + } + + /** + * Register the routes for products. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'type' => 'boolean', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_product( $id ); + } + + /** + * Prepare a single product output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = $this->get_product_data( $object ); + + // Add variations to variable products. + if ( $object->is_type( 'variable' ) && $object->has_child() ) { + $data['variations'] = $object->get_children(); + } + + // Add grouped products data. + if ( $object->is_type( 'grouped' ) && $object->has_child() ) { + $data['grouped_products'] = $object->get_children(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); + } + + // Filter product in stock or out of stock. + if ( is_bool( $request['in_stock'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_stock_status', + 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', + ) ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $args[ $on_sale_key ] += wc_get_product_ids_on_sale(); + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + return $args; + } + + /** + * Get the downloads for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_downloads() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param string $taxonomy Taxonomy slug. + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + return $terms; + } + + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + // Add featured image. + if ( has_post_thumbnail( $product->get_id() ) ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + $images[] = array( + 'id' => 0, + 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), // Default to now. + 'date_created_gmt' => wc_rest_prepare_date_response( current_time( 'timestamp', true ) ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( current_time( 'timestamp', true ) ), + 'src' => wc_placeholder_img_src(), + 'name' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get attribute taxonomy label. + * + * @deprecated 3.0.0 + * + * @param string $name Taxonomy name. + * @return string + */ + protected function get_attribute_taxonomy_label( $name ) { + $tax = get_taxonomy( $name ); + $labels = get_taxonomy_labels( $tax ); + + return $labels->singular_name; + } + + /** + * Get product attribute taxonomy name. + * + * @since 3.0.0 + * @param string $slug Taxonomy name. + * @param WC_Product $product Product data. + * @return string + */ + protected function get_attribute_taxonomy_name( $slug, $product ) { + $attributes = $product->get_attributes(); + + if ( ! isset( $attributes[ $slug ] ) ) { + return str_replace( 'pa_', '', $slug ); + } + + $attribute = $attributes[ $slug ]; + + // Taxonomy attribute name. + if ( $attribute->is_taxonomy() ) { + $taxonomy = $attribute->get_taxonomy_object(); + return $taxonomy->attribute_label; + } + + // Custom product attribute name. + return $attribute->get_name(); + } + + /** + * Get default attributes. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { + if ( 0 === strpos( $key, 'pa_' ) ) { + $default[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $key ), + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } else { + $default[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id Product ID. + * @param array $attribute Attribute data. + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + $_product = wc_get_product( $product->get_parent_id() ); + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + $name = str_replace( 'attribute_', '', $attribute_name ); + + if ( ! $attribute ) { + continue; + } + + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $name ); + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $name ), + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $attribute, + ); + } + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'id' => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0, + 'name' => $this->get_attribute_taxonomy_name( $attribute['name'], $product ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_product_data( $product ) { + $data = array( + 'id' => $product->get_id(), + 'name' => $product->get_name(), + 'slug' => $product->get_slug(), + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $product->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $product->get_date_modified() ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'description' => wpautop( do_shortcode( $product->get_description() ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', + 'date_on_sale_from' => wc_rest_prepare_date_response( $product->get_date_on_sale_from(), false ), + 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $product->get_date_on_sale_from() ), + 'date_on_sale_to' => wc_rest_prepare_date_response( $product->get_date_on_sale_to(), false ), + 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $product->get_date_on_sale_to() ), + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale(), + 'purchasable' => $product->is_purchasable(), + 'total_sales' => $product->get_total_sales(), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'manage_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders' => $product->get_backorders(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'weight' => $product->get_weight(), + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => $product->get_shipping_class_id(), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'parent_id' => $product->get_parent_id(), + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'categories' => $this->get_taxonomy_terms( $product ), + 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), + 'images' => $this->get_images( $product ), + 'attributes' => $this->get_attributes( $product ), + 'default_attributes' => $this->get_default_attributes( $product ), + 'variations' => array(), + 'grouped_products' => array(), + 'menu_order' => $product->get_menu_order(), + 'meta_data' => $product->get_meta_data(), + ); + + return $data; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + if ( 'variation' === $product->get_type() ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + } + + // Product parent ID for groups. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + // Set children for a grouped product. + if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { + $product->set_children( $request['grouped_products'] ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Allow set meta_data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); + } + + /** + * Set product images. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); + } + } + + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Save product shipping data. + * + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * @return WC_Product + */ + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( $data['dimensions']['height'] ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( $data['dimensions']['width'] ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( $data['dimensions']['length'] ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + $shipping_class_term = get_term_by( 'slug', wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); + + if ( $shipping_class_term ) { + $product->set_shipping_class_id( $shipping_class_term->term_id ); + } + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Clear caches here so in sync with any new variations/children. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_id() ); + wp_cache_delete( 'product-' . $object->get_id(), 'products' ); + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( 'variation' === $object->get_type() ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $object->is_type( 'variable' ) ) { + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->delete( true ); + } + } elseif ( $object->is_type( 'grouped' ) ) { + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->set_parent_id( 0 ); + $child->save(); + } + } + + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( "The date the product was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( "The date the product was last modified, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_keys( get_post_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( "The date the image was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( "The date the image was last modified, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + 'readonly' => true, + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['slug'] = array( + 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), array_keys( get_post_statuses() ) ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['type'] = array( + 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_types() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['featured'] = array( + 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( + 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute'] = array( + 'description' => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute_term'] = array( + 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + + if ( wc_tax_enabled() ) { + $params['tax_class'] = array( + 'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + + $params['in_stock'] = array( + 'description' => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['on_sale'] = array( + 'description' => __( 'Limit result set to products on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['min_price'] = array( + 'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['max_price'] = array( + 'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/class-wc-rest-report-sales-controller.php b/includes/api/class-wc-rest-report-sales-controller.php new file mode 100644 index 00000000000..37565b87e47 --- /dev/null +++ b/includes/api/class-wc-rest-report-sales-controller.php @@ -0,0 +1,31 @@ +[\w-]+)'; + + /** + * Register routes. + * + * @since 3.0.0 + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), + 'type' => 'string', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Return a single setting. + * + * @since 3.0.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $setting = $this->get_setting( $request['group_id'], $request['id'] ); + + if ( is_wp_error( $setting ) ) { + return $setting; + } + + $response = $this->prepare_item_for_response( $setting, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Return all settings in a group. + * + * @since 3.0.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $settings = $this->get_group_settings( $request['group_id'] ); + + if ( is_wp_error( $settings ) ) { + return $settings; + } + + $data = array(); + + foreach ( $settings as $setting_obj ) { + $setting = $this->prepare_item_for_response( $setting_obj, $request ); + $setting = $this->prepare_response_for_collection( $setting ); + if ( $this->is_setting_type_valid( $setting['type'] ) ) { + $data[] = $setting; + } + } + + return rest_ensure_response( $data ); + } + + /** + * Get all settings in a group. + * + * @since 3.0.0 + * @param string $group_id Group ID. + * @return array|WP_Error + */ + public function get_group_settings( $group_id ) { + if ( empty( $group_id ) ) { + return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); + + if ( empty( $settings ) ) { + return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $filtered_settings = array(); + foreach ( $settings as $setting ) { + $option_key = $setting['option_key']; + $setting = $this->filter_setting( $setting ); + $default = isset( $setting['default'] ) ? $setting['default'] : ''; + // Get the option value + if ( is_array( $option_key ) ) { + $option = get_option( $option_key[0] ); + $setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default; + } else { + $admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default ); + $setting['value'] = $admin_setting_value; + } + + if ( 'multi_select_countries' === $setting['type'] ) { + $setting['options'] = WC()->countries->get_countries(); + $setting['type'] = 'multiselect'; + } elseif ( 'single_select_country' === $setting['type'] ) { + $setting['type'] = 'select'; + $setting['options'] = $this->get_countries_and_states(); + } + + $filtered_settings[] = $setting; + } + + return $filtered_settings; + } + + /** + * Returns a list of countries and states for use in the base location setting. + * + * @since 3.0.7 + * @return array Array of states and countries. + */ + private function get_countries_and_states() { + $countries = WC()->countries->get_countries(); + if ( ! $countries ) { + return array(); + } + + $output = array(); + + foreach ( $countries as $key => $value ) { + if ( $states = WC()->countries->get_states( $key ) ) { + foreach ( $states as $state_key => $state_value ) { + $output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value; + } + } else { + $output[ $key ] = $value; + } + } + + return $output; + } + + /** + * Get setting data. + * + * @since 3.0.0 + * @param string $group_id Group ID. + * @param string $setting_id Setting ID. + * @return stdClass|WP_Error + */ + public function get_setting( $group_id, $setting_id ) { + if ( empty( $setting_id ) ) { + return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $settings = $this->get_group_settings( $group_id ); + + if ( is_wp_error( $settings ) ) { + return $settings; + } + + $array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id ); + + if ( empty( $array_key ) ) { + return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $setting = $settings[ $array_key[0] ]; + + if ( ! $this->is_setting_type_valid( $setting['type'] ) ) { + return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $setting; + } + + /** + * Bulk create, update and delete items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + // Get the request params. + $items = array_filter( $request->get_params() ); + + /* + * Since our batch settings update is group-specific and matches based on the route, + * we inject the URL parameters (containing group) into the batch items + */ + if ( ! empty( $items['update'] ) ) { + $to_update = array(); + foreach ( $items['update'] as $item ) { + $to_update[] = array_merge( $request->get_url_params(), $item ); + } + $request = new WP_REST_Request( $request->get_method() ); + $request->set_body_params( array( 'update' => $to_update ) ); + } + + return parent::batch_items( $request ); + } + + /** + * Update a single setting in a group. + * + * @since 3.0.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $setting = $this->get_setting( $request['group_id'], $request['id'] ); + + if ( is_wp_error( $setting ) ) { + return $setting; + } + + if ( is_callable( array( $this, 'validate_setting_' . $setting['type'] . '_field' ) ) ) { + $value = $this->{'validate_setting_' . $setting['type'] . '_field'}( $request['value'], $setting ); + } else { + $value = $this->validate_setting_text_field( $request['value'], $setting ); + } + + if ( is_wp_error( $value ) ) { + return $value; + } + + if ( is_array( $setting['option_key'] ) ) { + $setting['value'] = $value; + $option_key = $setting['option_key']; + $prev = get_option( $option_key[0] ); + $prev[ $option_key[1] ] = $request['value']; + update_option( $option_key[0], $prev ); + } else { + $update_data = array(); + $update_data[ $setting['option_key'] ] = $value; + $setting['value'] = $value; + WC_Admin_Settings::save_fields( array( $setting ), $update_data ); + } + + $response = $this->prepare_item_for_response( $setting, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Prepare a single setting object for response. + * + * @since 3.0.0 + * @param object $item Setting object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + unset( $item['option_key'] ); + $data = $this->filter_setting( $item ); + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, empty( $request['context'] ) ? 'view' : $request['context'] ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $data['id'], $request['group_id'] ) ); + return $response; + } + + /** + * Prepare links for the request. + * + * @since 3.0.0 + * @param string $setting_id Setting ID. + * @param string $group_id Group ID. + * @return array Links for the given setting. + */ + protected function prepare_links( $setting_id, $group_id ) { + $base = str_replace( '(?P[\w-]+)', $group_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $base, $setting_id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + ); + + return $links; + } + + /** + * Makes sure the current user has access to READ the settings APIs. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Makes sure the current user has access to WRITE the settings APIs. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function update_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Filters out bad values from the settings array/filter so we + * only return known values via the API. + * + * @since 3.0.0 + * @param array $setting + * @return array + */ + public function filter_setting( $setting ) { + $setting = array_intersect_key( + $setting, + array_flip( array_filter( array_keys( $setting ), array( $this, 'allowed_setting_keys' ) ) ) + ); + + if ( empty( $setting['options'] ) ) { + unset( $setting['options'] ); + } + + if ( 'image_width' === $setting['type'] ) { + $setting = $this->cast_image_width( $setting ); + } + + return $setting; + } + + /** + * For image_width, Crop can return "0" instead of false -- so we want + * to make sure we return these consistently the same we accept them. + * + * @since 3.0.0 + * @param array $setting + * @return array + */ + public function cast_image_width( $setting ) { + foreach ( array( 'default', 'value' ) as $key ) { + if ( isset( $setting[ $key ] ) ) { + $setting[ $key ]['width'] = intval( $setting[ $key ]['width'] ); + $setting[ $key ]['height'] = intval( $setting[ $key ]['height'] ); + $setting[ $key ]['crop'] = (bool) $setting[ $key ]['crop']; + } + } + return $setting; + } + + /** + * Callback for allowed keys for each setting response. + * + * @since 3.0.0 + * @param string $key Key to check + * @return boolean + */ + public function allowed_setting_keys( $key ) { + return in_array( $key, array( + 'id', + 'label', + 'description', + 'default', + 'tip', + 'placeholder', + 'type', + 'options', + 'value', + 'option_key', + ) ); + } + + /** + * Boolean for if a setting type is a valid supported setting type. + * + * @since 3.0.0 + * @param string $type + * @return bool + */ + public function is_setting_type_valid( $type ) { + return in_array( $type, array( + 'text', // Validates with validate_setting_text_field. + 'email', // Validates with validate_setting_text_field. + 'number', // Validates with validate_setting_text_field. + 'color', // Validates with validate_setting_text_field. + 'password', // Validates with validate_setting_text_field. + 'textarea', // Validates with validate_setting_textarea_field. + 'select', // Validates with validate_setting_select_field. + 'multiselect', // Validates with validate_setting_multiselect_field. + 'radio', // Validates with validate_setting_radio_field (-> validate_setting_select_field). + 'checkbox', // Validates with validate_setting_checkbox_field. + 'image_width', // Validates with validate_setting_image_width_field. + ) ); + } + + /** + * Get the settings schema, conforming to JSON Schema. + * + * @since 3.0.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'setting', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Setting value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'default' => array( + 'description' => __( 'Default value for the setting.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tip' => array( + 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'placeholder' => array( + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Type of setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), + 'readonly' => true, + ), + 'options' => array( + 'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-settings-controller.php b/includes/api/class-wc-rest-settings-controller.php new file mode 100644 index 00000000000..02058ce519f --- /dev/null +++ b/includes/api/class-wc-rest-settings-controller.php @@ -0,0 +1,232 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get all settings groups items. + * + * @since 3.0.0 + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $groups = apply_filters( 'woocommerce_settings_groups', array() ); + if ( empty( $groups ) ) { + return new WP_Error( 'rest_setting_groups_empty', __( 'No setting groups have been registered.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $defaults = $this->group_defaults(); + $filtered_groups = array(); + foreach ( $groups as $group ) { + $sub_groups = array(); + foreach ( $groups as $_group ) { + if ( ! empty( $_group['parent_id'] ) && $group['id'] === $_group['parent_id'] ) { + $sub_groups[] = $_group['id']; + } + } + $group['sub_groups'] = $sub_groups; + + $group = wp_parse_args( $group, $defaults ); + if ( ! is_null( $group['id'] ) && ! is_null( $group['label'] ) ) { + $group_obj = $this->filter_group( $group ); + $group_data = $this->prepare_item_for_response( $group_obj, $request ); + $group_data = $this->prepare_response_for_collection( $group_data ); + + $filtered_groups[] = $group_data; + } + } + + $response = rest_ensure_response( $filtered_groups ); + return $response; + } + + /** + * Prepare links for the request. + * + * @param string $group_id Group ID. + * @return array Links for the given group. + */ + protected function prepare_links( $group_id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'options' => array( + 'href' => rest_url( trailingslashit( $base ) . $group_id ), + ), + ); + + return $links; + } + + /** + * Prepare a report sales object for serialization. + * + * @since 3.0.0 + * @param array $item Group object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item['id'] ) ); + + return $response; + } + + /** + * Filters out bad values from the groups array/filter so we + * only return known values via the API. + * + * @since 3.0.0 + * @param array $group + * @return array + */ + public function filter_group( $group ) { + return array_intersect_key( + $group, + array_flip( array_filter( array_keys( $group ), array( $this, 'allowed_group_keys' ) ) ) + ); + } + + /** + * Callback for allowed keys for each group response. + * + * @since 3.0.0 + * @param string $key Key to check + * @return boolean + */ + public function allowed_group_keys( $key ) { + return in_array( $key, array( 'id', 'label', 'description', 'parent_id', 'sub_groups' ) ); + } + + /** + * Returns default settings for groups. null means the field is required. + * + * @since 3.0.0 + * @return array + */ + protected function group_defaults() { + return array( + 'id' => null, + 'label' => null, + 'description' => '', + 'parent_id' => '', + 'sub_groups' => array(), + ); + } + + /** + * Makes sure the current user has access to READ the settings APIs. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get the groups schema, conforming to JSON Schema. + * + * @since 3.0.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'setting_group', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'ID of parent grouping.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'sub_groups' => array( + 'description' => __( 'IDs for settings sub groups.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-shipping-methods-controller.php b/includes/api/class-wc-rest-shipping-methods-controller.php new file mode 100644 index 00000000000..e5ef36a674a --- /dev/null +++ b/includes/api/class-wc-rest-shipping-methods-controller.php @@ -0,0 +1,229 @@ + + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to view shipping methods. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to read a shipping method. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get shipping methods. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $wc_shipping = WC_Shipping::instance(); + $response = array(); + foreach ( $wc_shipping->get_shipping_methods() as $id => $shipping_method ) { + $method = $this->prepare_item_for_response( $shipping_method, $request ); + $method = $this->prepare_response_for_collection( $method ); + $response[] = $method; + } + return rest_ensure_response( $response ); + } + + /** + * Get a single Shipping Method. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $wc_shipping = WC_Shipping::instance(); + $methods = $wc_shipping->get_shipping_methods(); + if ( empty( $methods[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_shipping_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $method = $methods[ $request['id'] ]; + $response = $this->prepare_item_for_response( $method, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Prepare a shipping method for response. + * + * @param WC_Shipping_Method $method Shipping method object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $method, $request ) { + $data = array( + 'id' => $method->id, + 'title' => $method->method_title, + 'description' => $method->method_description, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $method, $request ) ); + + /** + * Filter shipping methods object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Shipping_Method $method Shipping method object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_shipping_method', $response, $method, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Shipping_Method $method Shipping method object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $method, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $method->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the shipping method schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'shipping_method', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'title' => array( + 'description' => __( 'Shipping method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Shipping method description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/class-wc-rest-shipping-zone-locations-controller.php b/includes/api/class-wc-rest-shipping-zone-locations-controller.php new file mode 100644 index 00000000000..71c867bc670 --- /dev/null +++ b/includes/api/class-wc-rest-shipping-zone-locations-controller.php @@ -0,0 +1,192 @@ +/locations endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Shipping Zone Locations class. + * + * @package WooCommerce/API + * @extends WC_REST_Shipping_Zones_Controller_Base + */ +class WC_REST_Shipping_Zone_Locations_Controller extends WC_REST_Shipping_Zones_Controller_Base { + + /** + * Register the routes for Shipping Zone Locations. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)/locations', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique ID for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_items' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get all Shipping Zone Locations. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $zone = $this->get_zone( (int) $request['id'] ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $locations = $zone->get_zone_locations(); + $data = array(); + + foreach ( $locations as $location_obj ) { + $location = $this->prepare_item_for_response( $location_obj, $request ); + $location = $this->prepare_response_for_collection( $location ); + $data[] = $location; + } + + return rest_ensure_response( $data ); + } + + /** + * Update all Shipping Zone Locations. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function update_items( $request ) { + $zone = $this->get_zone( (int) $request['id'] ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + if ( 0 === $zone->get_id() ) { + return new WP_Error( "woocommerce_rest_shipping_zone_locations_invalid_zone", __( 'The "rest of the world" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) ); + } + + $raw_locations = $request->get_json_params(); + $locations = array(); + + foreach ( (array) $raw_locations as $raw_location ) { + if ( empty( $raw_location['code'] ) ) { + continue; + } + + $type = ! empty( $raw_location['type'] ) ? sanitize_text_field( $raw_location['type'] ) : 'country'; + + if ( ! in_array( $type, array( 'postcode', 'state', 'country', 'continent' ), true ) ) { + continue; + } + + $locations[] = array( + 'code' => sanitize_text_field( $raw_location['code'] ), + 'type' => sanitize_text_field( $type ), + ); + } + + $zone->set_locations( $locations ); + $zone->save(); + + return $this->get_items( $request ); + } + + /** + * Prepare the Shipping Zone Location for the REST response. + * + * @param array $item Shipping Zone Location. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( (int) $request['id'] ) ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param int $zone_id Given Shipping Zone ID. + * @return array Links for the given Shipping Zone Location. + */ + protected function prepare_links( $zone_id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id; + $links = array( + 'collection' => array( + 'href' => rest_url( $base . '/locations' ), + ), + 'describes' => array( + 'href' => rest_url( $base ), + ), + ); + + return $links; + } + + /** + * Get the Shipping Zone Locations schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'shipping_zone_location', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'description' => __( 'Shipping zone location code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'type' => array( + 'description' => __( 'Shipping zone location type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'country', + 'enum' => array( + 'postcode', + 'state', + 'country', + 'continent', + ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-shipping-zone-methods-controller.php b/includes/api/class-wc-rest-shipping-zone-methods-controller.php new file mode 100644 index 00000000000..69d865a7e0d --- /dev/null +++ b/includes/api/class-wc-rest-shipping-zone-methods-controller.php @@ -0,0 +1,539 @@ +/methods endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Shipping Zone Methods class. + * + * @package WooCommerce/API + * @extends WC_REST_Shipping_Zones_Controller_Base + */ +class WC_REST_Shipping_Zone_Methods_Controller extends WC_REST_Shipping_Zones_Controller_Base { + + /** + * Register the routes for Shipping Zone Methods. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)/methods', array( + 'args' => array( + 'zone_id' => array( + 'description' => __( 'Unique ID for the zone.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'method_id' => array( + 'required' => true, + 'readonly' => false, + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)/methods/(?P[\d]+)', array( + 'args' => array( + 'zone_id' => array( + 'description' => __( 'Unique ID for the zone.', 'woocommerce' ), + 'type' => 'integer', + ), + 'instance_id' => array( + 'description' => __( 'Unique ID for the instance.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_items_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get a single Shipping Zone Method. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $zone = $this->get_zone( $request['zone_id'] ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $instance_id = (int) $request['instance_id']; + $methods = $zone->get_shipping_methods(); + $method = false; + + foreach ( $methods as $method_obj ) { + if ( $instance_id === $method_obj->instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_item_for_response( $method, $request ); + + return rest_ensure_response( $data ); + } + + /** + * Get all Shipping Zone Methods. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $zone = $this->get_zone( $request['zone_id'] ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $methods = $zone->get_shipping_methods(); + $data = array(); + + foreach ( $methods as $method_obj ) { + $method = $this->prepare_item_for_response( $method_obj, $request ); + $data[] = $method; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a new shipping zone method instance. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + $method_id = $request['method_id']; + $zone = $this->get_zone( $request['zone_id'] ); + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $instance_id = $zone->add_shipping_method( $method_id ); + $methods = $zone->get_shipping_methods(); + $method = false; + foreach ( $methods as $method_obj ) { + if ( $instance_id === $method_obj->instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( 'Resource cannot be created.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $method = $this->update_fields( $instance_id, $method, $request ); + if ( is_wp_error( $method ) ) { + return $method; + } + + $data = $this->prepare_item_for_response( $method, $request ); + return rest_ensure_response( $data ); + } + + /** + * Delete a shipping method instance. + * + * @param WP_REST_Request $request Full details about the request + * @return WP_Error|boolean + */ + public function delete_item( $request ) { + $zone = $this->get_zone( $request['zone_id'] ); + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $instance_id = (int) $request['instance_id']; + $force = $request['force']; + + $methods = $zone->get_shipping_methods(); + $method = false; + + foreach ( $methods as $method_obj ) { + if ( $instance_id === $method_obj->instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $method = $this->update_fields( $instance_id, $method, $request ); + if ( is_wp_error( $method ) ) { + return $method; + } + + $request->set_param( 'context', 'view' ); + $response = $this->prepare_item_for_response( $method, $request ); + + // Actually delete + if ( $force ) { + $zone->delete_shipping_method( $instance_id ); + } else { + return new WP_Error( 'rest_trash_not_supported', __( 'Shipping methods do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + /** + * Fires after a product review is deleted via the REST API. + * + * @param object $method + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'rest_delete_product_review', $method, $response, $request ); + + return $response; + } + + /** + * Update A Single Shipping Zone Method. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function update_item( $request ) { + $zone = $this->get_zone( $request['zone_id'] ); + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $instance_id = (int) $request['instance_id']; + $methods = $zone->get_shipping_methods(); + $method = false; + + foreach ( $methods as $method_obj ) { + if ( $instance_id === $method_obj->instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $method = $this->update_fields( $instance_id, $method, $request ); + if ( is_wp_error( $method ) ) { + return $method; + } + + $data = $this->prepare_item_for_response( $method, $request ); + return rest_ensure_response( $data ); + } + + /** + * Updates settings, order, and enabled status on create. + * + * @param int $instance_id integer + * @param WC_Shipping_Method $method + * @param WP_REST_Request $request + * + * @return WC_Shipping_Method + */ + public function update_fields( $instance_id, $method, $request ) { + global $wpdb; + + // Update settings if present + if ( isset( $request['settings'] ) ) { + $method->init_instance_settings(); + $instance_settings = $method->instance_settings; + $errors_found = false; + foreach ( $method->get_instance_form_fields() as $key => $field ) { + if ( isset( $request['settings'][ $key ] ) ) { + if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) { + $value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field ); + } else { + $value = $this->validate_setting_text_field( $request['settings'][ $key ], $field ); + } + if ( is_wp_error( $value ) ) { + $errors_found = true; + break; + } + $instance_settings[ $key ] = $value; + } + } + + if ( $errors_found ) { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + update_option( $method->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $method->id . '_instance_settings_values', $instance_settings, $method ) ); + } + + // Update order + if ( isset( $request['order'] ) ) { + $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $request['order'] ) ), array( 'instance_id' => absint( $instance_id ) ) ); + $method->method_order = absint( $request['order'] ); + } + + // Update if this method is enabled or not. + if ( isset( $request['enabled'] ) ) { + if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $request['enabled'] ), array( 'instance_id' => absint( $instance_id ) ) ) ) { + do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method->id, $request['zone_id'], $request['enabled'] ); + $method->enabled = ( true === $request['enabled'] ? 'yes' : 'no' ); + } + } + + return $method; + } + + /** + * Prepare the Shipping Zone Method for the REST response. + * + * @param array $item Shipping Zone Method. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $method = array( + 'id' => $item->instance_id, + 'instance_id' => $item->instance_id, + 'title' => $item->instance_settings['title'], + 'order' => $item->method_order, + 'enabled' => ( 'yes' === $item->enabled ), + 'method_id' => $item->id, + 'method_title' => $item->method_title, + 'method_description' => $item->method_description, + 'settings' => $this->get_settings( $item ), + ); + + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $method, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $request['zone_id'], $item->instance_id ) ); + + $response = $this->prepare_response_for_collection( $response ); + + return $response; + } + + /** + * Return settings associated with this shipping zone method instance. + * + * @param WC_Shipping_Method $item + * + * @return array + */ + public function get_settings( $item ) { + $item->init_instance_settings(); + $settings = array(); + foreach ( $item->get_instance_form_fields() as $id => $field ) { + $data = array( + 'id' => $id, + 'label' => $field['title'], + 'description' => empty( $field['description'] ) ? '' : $field['description'], + 'type' => $field['type'], + 'value' => $item->instance_settings[ $id ], + 'default' => empty( $field['default'] ) ? '' : $field['default'], + 'tip' => empty( $field['description'] ) ? '' : $field['description'], + 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + return $settings; + } + + /** + * Prepare links for the request. + * + * @param int $zone_id Given Shipping Zone ID. + * @param int $instance_id Given Shipping Zone Method Instance ID. + * @return array Links for the given Shipping Zone Method. + */ + protected function prepare_links( $zone_id, $instance_id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id; + $links = array( + 'self' => array( + 'href' => rest_url( $base . '/methods/' . $instance_id ), + ), + 'collection' => array( + 'href' => rest_url( $base . '/methods' ), + ), + 'describes' => array( + 'href' => rest_url( $base ), + ), + ); + + return $links; + } + + /** + * Get the Shipping Zone Methods schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'shipping_zone_method', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Shipping method instance ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'instance_id' => array( + 'description' => __( 'Shipping method instance ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'title' => array( + 'description' => __( 'Shipping method customer facing title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'order' => array( + 'description' => __( 'Shipping method sort order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'enabled' => array( + 'description' => __( 'Shipping method enabled status.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_description' => array( + 'description' => __( 'Shipping method description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'settings' => array( + 'description' => __( 'Shipping method settings.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Type of setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Setting value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'default' => array( + 'description' => __( 'Default value for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tip' => array( + 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'placeholder' => array( + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-shipping-zones-controller.php b/includes/api/class-wc-rest-shipping-zones-controller.php new file mode 100644 index 00000000000..c83e7dd8695 --- /dev/null +++ b/includes/api/class-wc-rest-shipping-zones-controller.php @@ -0,0 +1,302 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'name' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Shipping zone name.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d-]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique ID for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_items_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get a single Shipping Zone. + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $zone = $this->get_zone( $request->get_param( 'id' ) ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $data = $zone->get_data(); + $data = $this->prepare_item_for_response( $data, $request ); + $data = $this->prepare_response_for_collection( $data ); + + return rest_ensure_response( $data ); + } + + /** + * Get all Shipping Zones. + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public function get_items( $request ) { + $rest_of_the_world = WC_Shipping_Zones::get_zone_by( 'zone_id', 0 ); + + $zones = WC_Shipping_Zones::get_zones(); + array_unshift( $zones, $rest_of_the_world->get_data() ); + $data = array(); + + foreach ( $zones as $zone_obj ) { + $zone = $this->prepare_item_for_response( $zone_obj, $request ); + $zone = $this->prepare_response_for_collection( $zone ); + $data[] = $zone; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single Shipping Zone. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + $zone = new WC_Shipping_Zone( null ); + + if ( ! is_null( $request->get_param( 'name' ) ) ) { + $zone->set_zone_name( $request->get_param( 'name' ) ); + } + + if ( ! is_null( $request->get_param( 'order' ) ) ) { + $zone->set_zone_order( $request->get_param( 'order' ) ); + } + + $zone->save(); + + if ( $zone->get_id() !== 0 ) { + $request->set_param( 'id', $zone->get_id() ); + $response = $this->get_item( $request ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $zone->get_id() ) ) ); + return $response; + } else { + return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( "Resource cannot be created. Check to make sure 'order' and 'name' are present.", 'woocommerce' ), array( 'status' => 500 ) ); + } + } + + /** + * Update a single Shipping Zone. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + $zone = $this->get_zone( $request->get_param( 'id' ) ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + if ( 0 === $zone->get_id() ) { + return new WP_Error( "woocommerce_rest_shipping_zone_invalid_zone", __( 'The "rest of the world" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) ); + } + + $zone_changed = false; + + if ( ! is_null( $request->get_param( 'name' ) ) ) { + $zone->set_zone_name( $request->get_param( 'name' ) ); + $zone_changed = true; + } + + if ( ! is_null( $request->get_param( 'order' ) ) ) { + $zone->set_zone_order( $request->get_param( 'order' ) ); + $zone_changed = true; + } + + if ( $zone_changed ) { + $zone->save(); + } + + return $this->get_item( $request ); + } + + /** + * Delete a single Shipping Zone. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function delete_item( $request ) { + $zone = $this->get_zone( $request->get_param( 'id' ) ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $force = $request['force']; + + $response = $this->get_item( $request ); + + if ( $force ) { + $zone->delete(); + } else { + return new WP_Error( 'rest_trash_not_supported', __( 'Shipping zones do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + return $response; + } + + /** + * Prepare the Shipping Zone for the REST response. + * + * @param array $item Shipping Zone. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item['id'], + 'name' => $item['zone_name'], + 'order' => (int) $item['zone_order'], + ); + + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $data['id'] ) ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param int $zone_id Given Shipping Zone ID. + * @return array Links for the given Shipping Zone. + */ + protected function prepare_links( $zone_id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $zone_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + 'describedby' => array( + 'href' => rest_url( trailingslashit( $base ) . $zone_id . '/locations' ), + ), + ); + + return $links; + } + + /** + * Get the Shipping Zones schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'shipping_zone', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Shipping zone name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'order' => array( + 'description' => __( 'Shipping zone order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-system-status-controller.php b/includes/api/class-wc-rest-system-status-controller.php new file mode 100644 index 00000000000..b05ac59dea1 --- /dev/null +++ b/includes/api/class-wc-rest-system-status-controller.php @@ -0,0 +1,968 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to view system status. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get a system status info, by section. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $schema = $this->get_item_schema(); + $mappings = $this->get_item_mappings(); + $response = array(); + + foreach ( $mappings as $section => $values ) { + foreach ( $values as $key => $value ) { + if ( isset( $schema['properties'][ $section ]['properties'][ $key ]['type'] ) ) { + settype( $values[ $key ], $schema['properties'][ $section ]['properties'][ $key ]['type'] ); + } + } + settype( $values, $schema['properties'][ $section ]['type'] ); + $response[ $section ] = $values; + } + + $response = $this->prepare_item_for_response( $response, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Get the system status schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'system_status', + 'type' => 'object', + 'properties' => array( + 'environment' => array( + 'description' => __( 'Environment.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'home_url' => array( + 'description' => __( 'Home URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'site_url' => array( + 'description' => __( 'Site URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wc_version' => array( + 'description' => __( 'WooCommerce version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'log_directory' => array( + 'description' => __( 'Log directory.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'log_directory_writable' => array( + 'description' => __( 'Is log directory writable?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_version' => array( + 'description' => __( 'WordPress version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_multisite' => array( + 'description' => __( 'Is WordPress multisite?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_memory_limit' => array( + 'description' => __( 'WordPress memory limit.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_debug_mode' => array( + 'description' => __( 'Is WordPress debug mode active?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_cron' => array( + 'description' => __( 'Are WordPress cron jobs enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'language' => array( + 'description' => __( 'WordPress language.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'server_info' => array( + 'description' => __( 'Server info.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'php_version' => array( + 'description' => __( 'PHP version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'php_post_max_size' => array( + 'description' => __( 'PHP post max size.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'php_max_execution_time' => array( + 'description' => __( 'PHP max execution time.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'php_max_input_vars' => array( + 'description' => __( 'PHP max input vars.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'curl_version' => array( + 'description' => __( 'cURL version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'suhosin_installed' => array( + 'description' => __( 'Is SUHOSIN installed?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'max_upload_size' => array( + 'description' => __( 'Max upload size.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'mysql_version' => array( + 'description' => __( 'MySQL version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'default_timezone' => array( + 'description' => __( 'Default timezone.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'fsockopen_or_curl_enabled' => array( + 'description' => __( 'Is fsockopen/cURL enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'soapclient_enabled' => array( + 'description' => __( 'Is SoapClient class enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'domdocument_enabled' => array( + 'description' => __( 'Is DomDocument class enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'gzip_enabled' => array( + 'description' => __( 'Is GZip enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'mbstring_enabled' => array( + 'description' => __( 'Is mbstring enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'remote_post_successful' => array( + 'description' => __( 'Remote POST successful?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'remote_post_response' => array( + 'description' => __( 'Remote POST response.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'remote_get_successful' => array( + 'description' => __( 'Remote GET successful?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'remote_get_response' => array( + 'description' => __( 'Remote GET response.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + 'database' => array( + 'description' => __( 'Database.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'wc_database_version' => array( + 'description' => __( 'WC database version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'database_prefix' => array( + 'description' => __( 'Database prefix.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'maxmind_geoip_database' => array( + 'description' => __( 'MaxMind GeoIP database.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'database_tables' => array( + 'description' => __( 'Database tables.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'active_plugins' => array( + 'description' => __( 'Active plugins.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'theme' => array( + 'description' => __( 'Theme.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'name' => array( + 'description' => __( 'Theme name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'version' => array( + 'description' => __( 'Theme version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'version_latest' => array( + 'description' => __( 'Latest version of theme.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'author_url' => array( + 'description' => __( 'Theme author URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'is_child_theme' => array( + 'description' => __( 'Is this theme a child theme?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'has_woocommerce_support' => array( + 'description' => __( 'Does the theme declare WooCommerce support?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'has_woocommerce_file' => array( + 'description' => __( 'Does the theme have a woocommerce.php file?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'has_outdated_templates' => array( + 'description' => __( 'Does this theme have outdated templates?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'overrides' => array( + 'description' => __( 'Template overrides.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'parent_name' => array( + 'description' => __( 'Parent theme name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'parent_version' => array( + 'description' => __( 'Parent theme version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'parent_author_url' => array( + 'description' => __( 'Parent theme author URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + 'settings' => array( + 'description' => __( 'Settings.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'api_enabled' => array( + 'description' => __( 'REST API enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'force_ssl' => array( + 'description' => __( 'SSL forced?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency' => array( + 'description' => __( 'Currency.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_symbol' => array( + 'description' => __( 'Currency symbol.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_position' => array( + 'description' => __( 'Currency position.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'thousand_separator' => array( + 'description' => __( 'Thousand separator.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'decimal_separator' => array( + 'description' => __( 'Decimal separator.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'number_of_decimals' => array( + 'description' => __( 'Number of decimals.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'geolocation_enabled' => array( + 'description' => __( 'Geolocation enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'taxonomies' => array( + 'description' => __( 'Taxonomy terms for product/order statuses.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'product_visibility_terms' => array( + 'description' => __( 'Terms in the product visibility taxonomy.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'security' => array( + 'description' => __( 'Security.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'secure_connection' => array( + 'description' => __( 'Is the connection to your store secure?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'hide_errors' => array( + 'description' => __( 'Hide errors from visitors?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + 'pages' => array( + 'description' => __( 'WooCommerce pages.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Return an array of sections and the data associated with each. + * + * @return array + */ + public function get_item_mappings() { + return array( + 'environment' => $this->get_environment_info(), + 'database' => $this->get_database_info(), + 'active_plugins' => $this->get_active_plugins(), + 'theme' => $this->get_theme_info(), + 'settings' => $this->get_settings(), + 'security' => $this->get_security_info(), + 'pages' => $this->get_pages(), + ); + } + + /** + * Get array of environment information. Includes thing like software + * versions, and various server settings. + * + * @return array + */ + public function get_environment_info() { + global $wpdb; + + // Figure out cURL version, if installed. + $curl_version = ''; + if ( function_exists( 'curl_version' ) ) { + $curl_version = curl_version(); + $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version']; + } + + // WP memory limit + $wp_memory_limit = wc_let_to_num( WP_MEMORY_LIMIT ); + if ( function_exists( 'memory_get_usage' ) ) { + $wp_memory_limit = max( $wp_memory_limit, wc_let_to_num( @ini_get( 'memory_limit' ) ) ); + } + + // Test POST requests + $post_response = wp_safe_remote_post( 'https://www.paypal.com/cgi-bin/webscr', array( + 'timeout' => 10, + 'user-agent' => 'WooCommerce/' . WC()->version, + 'httpversion' => '1.1', + 'body' => array( + 'cmd' => '_notify-validate', + ), + ) ); + $post_response_successful = false; + if ( ! is_wp_error( $post_response ) && $post_response['response']['code'] >= 200 && $post_response['response']['code'] < 300 ) { + $post_response_successful = true; + } + + // Test GET requests + $get_response = wp_safe_remote_get( 'https://woocommerce.com/wc-api/product-key-api?request=ping&network=' . ( is_multisite() ? '1' : '0' ) ); + $get_response_successful = false; + if ( ! is_wp_error( $post_response ) && $post_response['response']['code'] >= 200 && $post_response['response']['code'] < 300 ) { + $get_response_successful = true; + } + + // Return all environment info. Described by JSON Schema. + return array( + 'home_url' => get_option( 'home' ), + 'site_url' => get_option( 'siteurl' ), + 'version' => WC()->version, + 'log_directory' => WC_LOG_DIR, + 'log_directory_writable' => ( @fopen( WC_LOG_DIR . 'test-log.log', 'a' ) ? true : false ), + 'wp_version' => get_bloginfo( 'version' ), + 'wp_multisite' => is_multisite(), + 'wp_memory_limit' => $wp_memory_limit, + 'wp_debug_mode' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ), + 'wp_cron' => ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ), + 'language' => get_locale(), + 'server_info' => $_SERVER['SERVER_SOFTWARE'], + 'php_version' => phpversion(), + 'php_post_max_size' => wc_let_to_num( ini_get( 'post_max_size' ) ), + 'php_max_execution_time' => ini_get( 'max_execution_time' ), + 'php_max_input_vars' => ini_get( 'max_input_vars' ), + 'curl_version' => $curl_version, + 'suhosin_installed' => extension_loaded( 'suhosin' ), + 'max_upload_size' => wp_max_upload_size(), + 'mysql_version' => ( ! empty( $wpdb->is_mysql ) ? $wpdb->db_version() : '' ), + 'default_timezone' => date_default_timezone_get(), + 'fsockopen_or_curl_enabled' => ( function_exists( 'fsockopen' ) || function_exists( 'curl_init' ) ), + 'soapclient_enabled' => class_exists( 'SoapClient' ), + 'domdocument_enabled' => class_exists( 'DOMDocument' ), + 'gzip_enabled' => is_callable( 'gzopen' ), + 'mbstring_enabled' => extension_loaded( 'mbstring' ), + 'remote_post_successful' => $post_response_successful, + 'remote_post_response' => ( is_wp_error( $post_response ) ? $post_response->get_error_message() : $post_response['response']['code'] ), + 'remote_get_successful' => $get_response_successful, + 'remote_get_response' => ( is_wp_error( $get_response ) ? $get_response->get_error_message() : $get_response['response']['code'] ), + ); + } + + /** + * Get array of database information. Version, prefix, and table existence. + * + * @return array + */ + public function get_database_info() { + global $wpdb; + + // WC Core tables to check existence of + $tables = apply_filters( 'woocommerce_database_tables', array( + 'woocommerce_sessions', + 'woocommerce_api_keys', + 'woocommerce_attribute_taxonomies', + 'woocommerce_downloadable_product_permissions', + 'woocommerce_order_items', + 'woocommerce_order_itemmeta', + 'woocommerce_tax_rates', + 'woocommerce_tax_rate_locations', + 'woocommerce_shipping_zones', + 'woocommerce_shipping_zone_locations', + 'woocommerce_shipping_zone_methods', + 'woocommerce_payment_tokens', + 'woocommerce_payment_tokenmeta', + ) ); + + if ( get_option( 'db_version' ) < 34370 ) { + $tables[] = 'woocommerce_termmeta'; + } + $table_exists = array(); + foreach ( $tables as $table ) { + $table_exists[ $table ] = ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s;", $wpdb->prefix . $table ) ) === $wpdb->prefix . $table ); + } + + // Return all database info. Described by JSON Schema. + return array( + 'wc_database_version' => get_option( 'woocommerce_db_version' ), + 'database_prefix' => $wpdb->prefix, + 'maxmind_geoip_database' => WC_Geolocation::get_local_database_path(), + 'database_tables' => $table_exists, + ); + } + + /** + * Get a list of plugins active on the site. + * + * @return array + */ + public function get_active_plugins() { + require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + require_once( ABSPATH . 'wp-admin/includes/update.php' ); + + if ( ! function_exists( 'get_plugin_updates' ) ) { + return array(); + } + + // Get both site plugins and network plugins + $active_plugins = (array) get_option( 'active_plugins', array() ); + if ( is_multisite() ) { + $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); + $active_plugins = array_merge( $active_plugins, $network_activated_plugins ); + } + + $active_plugins_data = array(); + $available_updates = get_plugin_updates(); + + foreach ( $active_plugins as $plugin ) { + $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $dirname = dirname( $plugin ); + $version_latest = ''; + $slug = explode( '/', $plugin ); + $slug = explode( '.', end( $slug ) ); + $slug = $slug[0]; + + if ( 'woocommerce' !== $slug && ( strstr( $data['PluginURI'], 'woothemes.com' ) || strstr( $data['PluginURI'], 'woocommerce.com' ) ) ) { + if ( false === ( $version_data = get_transient( md5( $plugin ) . '_version_data' ) ) ) { + $changelog = wp_safe_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $dirname . '/changelog.txt' ); + $cl_lines = explode( "\n", wp_remote_retrieve_body( $changelog ) ); + if ( ! empty( $cl_lines ) ) { + foreach ( $cl_lines as $line_num => $cl_line ) { + if ( preg_match( '/^[0-9]/', $cl_line ) ) { + $date = str_replace( '.' , '-' , trim( substr( $cl_line , 0 , strpos( $cl_line , '-' ) ) ) ); + $version = preg_replace( '~[^0-9,.]~' , '' ,stristr( $cl_line , "version" ) ); + $update = trim( str_replace( "*" , "" , $cl_lines[ $line_num + 1 ] ) ); + $version_data = array( 'date' => $date , 'version' => $version , 'update' => $update , 'changelog' => $changelog ); + set_transient( md5( $plugin ) . '_version_data', $version_data, DAY_IN_SECONDS ); + break; + } + } + } + } + $version_latest = $version_data['version']; + } elseif ( isset( $available_updates[ $plugin ]->update->new_version ) ) { + $version_latest = $available_updates[ $plugin ]->update->new_version; + } + + // convert plugin data to json response format. + $active_plugins_data[] = array( + 'plugin' => $plugin, + 'name' => $data['Name'], + 'version' => $data['Version'], + 'version_latest' => $version_latest, + 'url' => $data['PluginURI'], + 'author_name' => $data['AuthorName'], + 'author_url' => esc_url_raw( $data['AuthorURI'] ), + 'network_activated' => $data['Network'], + ); + } + + return $active_plugins_data; + } + + /** + * Get info on the current active theme, info on parent theme (if presnet) + * and a list of template overrides. + * + * @return array + */ + public function get_theme_info() { + $active_theme = wp_get_theme(); + + // Get parent theme info if this theme is a child theme, otherwise + // pass empty info in the response. + if ( is_child_theme() ) { + $parent_theme = wp_get_theme( $active_theme->Template ); + $parent_theme_info = array( + 'parent_name' => $parent_theme->Name, + 'parent_version' => $parent_theme->Version, + 'parent_version_latest' => WC_Admin_Status::get_latest_theme_version( $parent_theme ), + 'parent_author_url' => $parent_theme->{'Author URI'}, + ); + } else { + $parent_theme_info = array( 'parent_name' => '', 'parent_version' => '', 'parent_version_latest' => '', 'parent_author_url' => '' ); + } + + /** + * Scan the theme directory for all WC templates to see if our theme + * overrides any of them. + */ + $override_files = array(); + $outdated_templates = false; + $scan_files = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates/' ); + foreach ( $scan_files as $file ) { + if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . $file; + } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; + } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { + $theme_file = get_template_directory() . '/' . $file; + } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; + } else { + $theme_file = false; + } + + if ( ! empty( $theme_file ) ) { + $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file ); + $theme_version = WC_Admin_Status::get_file_version( $theme_file ); + if ( $core_version && ( empty( $theme_version ) || version_compare( $theme_version, $core_version, '<' ) ) ) { + if ( ! $outdated_templates ) { + $outdated_templates = true; + } + } + $override_files[] = array( + 'file' => str_replace( WP_CONTENT_DIR . '/themes/', '', $theme_file ), + 'version' => $theme_version, + 'core_version' => $core_version, + ); + } + } + + $active_theme_info = array( + 'name' => $active_theme->Name, + 'version' => $active_theme->Version, + 'version_latest' => WC_Admin_Status::get_latest_theme_version( $active_theme ), + 'author_url' => esc_url_raw( $active_theme->{'Author URI'} ), + 'is_child_theme' => is_child_theme(), + 'has_woocommerce_support' => ( current_theme_supports( 'woocommerce' ) || in_array( $active_theme->template, wc_get_core_supported_themes() ) ), + 'has_woocommerce_file' => ( file_exists( get_stylesheet_directory() . '/woocommerce.php' ) || file_exists( get_template_directory() . '/woocommerce.php' ) ), + 'has_outdated_templates' => $outdated_templates, + 'overrides' => $override_files, + ); + + return array_merge( $active_theme_info, $parent_theme_info ); + } + + /** + * Get some setting values for the site that are useful for debugging + * purposes. For full settings access, use the settings api. + * + * @return array + */ + public function get_settings() { + // Get a list of terms used for product/order taxonomies + $term_response = array(); + $terms = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); + foreach ( $terms as $term ) { + $term_response[ $term->slug ] = strtolower( $term->name ); + } + + // Get a list of terms used for product visibility. + $product_visibility_terms = array(); + $terms = get_terms( 'product_visibility', array( 'hide_empty' => 0 ) ); + foreach ( $terms as $term ) { + $product_visibility_terms[ $term->slug ] = strtolower( $term->name ); + } + + // Return array of useful settings for debugging. + return array( + 'api_enabled' => 'yes' === get_option( 'woocommerce_api_enabled' ), + 'force_ssl' => 'yes' === get_option( 'woocommerce_force_ssl_checkout' ), + 'currency' => get_woocommerce_currency(), + 'currency_symbol' => get_woocommerce_currency_symbol(), + 'currency_position' => get_option( 'woocommerce_currency_pos' ), + 'thousand_separator' => wc_get_price_thousand_separator(), + 'decimal_separator' => wc_get_price_decimal_separator(), + 'number_of_decimals' => wc_get_price_decimals(), + 'geolocation_enabled' => in_array( get_option( 'woocommerce_default_customer_address' ), array( 'geolocation_ajax', 'geolocation' ) ), + 'taxonomies' => $term_response, + 'product_visibility_terms' => $product_visibility_terms, + ); + } + + /** + * Returns security tips. + * + * @return array + */ + public function get_security_info() { + $check_page = 0 < wc_get_page_id( 'shop' ) ? get_permalink( wc_get_page_id( 'shop' ) ) : get_home_url(); + return array( + 'secure_connection' => 'https' === substr( $check_page, 0, 5 ), + 'hide_errors' => ! ( defined( 'WP_DEBUG' ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG && WP_DEBUG_DISPLAY ) || 0 === intval( ini_get( 'display_errors' ) ), + ); + } + + /** + * Returns a mini-report on WC pages and if they are configured correctly: + * Present, visible, and including the correct shortcode. + * + * @return array + */ + public function get_pages() { + // WC pages to check against + $check_pages = array( + _x( 'Shop base', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_shop_page_id', + 'shortcode' => '', + ), + _x( 'Cart', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_cart_page_id', + 'shortcode' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', + ), + _x( 'Checkout', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_checkout_page_id', + 'shortcode' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', + ), + _x( 'My account', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_myaccount_page_id', + 'shortcode' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']', + ), + ); + + $pages_output = array(); + foreach ( $check_pages as $page_name => $values ) { + $page_id = get_option( $values['option'] ); + $page_set = $page_exists = $page_visible = false; + $shortcode_present = $shortcode_required = false; + + // Page checks + if ( $page_id ) { + $page_set = true; + } + if ( get_post( $page_id ) ) { + $page_exists = true; + } + if ( 'publish' === get_post_status( $page_id ) ) { + $page_visible = true; + } + + // Shortcode checks + if ( $values['shortcode'] && get_post( $page_id ) ) { + $shortcode_required = true; + $page = get_post( $page_id ); + if ( strstr( $page->post_content, $values['shortcode'] ) ) { + $shortcode_present = true; + } + } + + // Wrap up our findings into an output array + $pages_output[] = array( + 'page_name' => $page_name, + 'page_id' => $page_id, + 'page_set' => $page_set, + 'page_exists' => $page_exists, + 'page_visible' => $page_visible, + 'shortcode' => $values['shortcode'], + 'shortcode_required' => $shortcode_required, + 'shortcode_present' => $shortcode_present, + ); + } + + return $pages_output; + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } + + /** + * Prepare the system status response + * + * @param array $system_status + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $system_status, $request ) { + $data = $this->add_additional_fields_to_object( $system_status, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + + $response = rest_ensure_response( $data ); + + /** + * Filter the system status returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param mixed $system_status System status + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_system_status', $response, $system_status, $request ); + } +} diff --git a/includes/api/class-wc-rest-system-status-tools-controller.php b/includes/api/class-wc-rest-system-status-tools-controller.php new file mode 100644 index 00000000000..dac9d5fe896 --- /dev/null +++ b/includes/api/class-wc-rest-system-status-tools-controller.php @@ -0,0 +1,490 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to view system status tools. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to view a specific system status tool. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to execute a specific system status tool. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * A list of avaiable tools for use in the system status section. + * 'button' becomes 'action' in the API. + * + * @return array + */ + public function get_tools() { + $tools = array( + 'clear_transients' => array( + 'name' => __( 'WC transients', 'woocommerce' ), + 'button' => __( 'Clear transients', 'woocommerce' ), + 'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ), + ), + 'clear_expired_transients' => array( + 'name' => __( 'Expired transients', 'woocommerce' ), + 'button' => __( 'Clear expired transients', 'woocommerce' ), + 'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ), + ), + 'delete_orphaned_variations' => array( + 'name' => __( 'Orphaned variations', 'woocommerce' ), + 'button' => __( 'Delete orphaned variations', 'woocommerce' ), + 'desc' => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ), + ), + 'add_order_indexes' => array( + 'name' => __( 'Order address indexes', 'woocommerce' ), + 'button' => __( 'Add order address indexes', 'woocommerce' ), + 'desc' => __( 'This tool will add address indexes to orders that do not have them yet. This improves order search results.', 'woocommerce' ), + ), + 'recount_terms' => array( + 'name' => __( 'Term counts', 'woocommerce' ), + 'button' => __( 'Recount terms', 'woocommerce' ), + 'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ), + ), + 'reset_roles' => array( + 'name' => __( 'Capabilities', 'woocommerce' ), + 'button' => __( 'Reset capabilities', 'woocommerce' ), + 'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ), + ), + 'clear_sessions' => array( + 'name' => __( 'Customer sessions', 'woocommerce' ), + 'button' => __( 'Clear all sessions', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will delete all customer session data from the database, including any current live carts.', 'woocommerce' ) + ), + ), + 'install_pages' => array( + 'name' => __( 'Install WooCommerce pages', 'woocommerce' ), + 'button' => __( 'Install pages', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' ) + ), + ), + 'delete_taxes' => array( + 'name' => __( 'Delete all WooCommerce tax rates', 'woocommerce' ), + 'button' => __( 'Delete ALL tax rates', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This option will delete ALL of your tax rates, use with caution.', 'woocommerce' ) + ), + ), + 'reset_tracking' => array( + 'name' => __( 'Reset usage tracking settings', 'woocommerce' ), + 'button' => __( 'Reset usage tracking settings', 'woocommerce' ), + 'desc' => __( 'This will reset your usage tracking settings, causing it to show the opt-in banner again and not sending any data.', 'woocommerce' ), + ), + ); + + return apply_filters( 'woocommerce_debug_tools', $tools ); + } + + /** + * Get a list of system status tools. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $tools = array(); + foreach ( $this->get_tools() as $id => $tool ) { + $tools[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( array( + 'id' => $id, + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ), $request ) ); + } + + $response = rest_ensure_response( $tools ); + return $response; + } + + /** + * Return a single tool. + * + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $tools = $this->get_tools(); + if ( empty( $tools[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + $tool = $tools[ $request['id'] ]; + return rest_ensure_response( $this->prepare_item_for_response( array( + 'id' => $request['id'], + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ), $request ) ); + } + + /** + * Update (execute) a tool. + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $tools = $this->get_tools(); + if ( empty( $tools[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tool = $tools[ $request['id'] ]; + $tool = array( + 'id' => $request['id'], + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ); + + $execute_return = $this->execute_tool( $request['id'] ); + $tool = array_merge( $tool, $execute_return ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tool, $request ); + return rest_ensure_response( $response ); + } + + /** + * Prepare a tool item for serialization. + * + * @param array $item Object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item['id'] ) ); + + return $response; + } + + /** + * Get the system status tools schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'system_status_tool', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the tool.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'name' => array( + 'description' => __( 'Tool name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'action' => array( + 'description' => __( 'What running the tool will do.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'description' => array( + 'description' => __( 'Tool description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'success' => array( + 'description' => __( 'Did the tool run successfully?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + ), + 'message' => array( + 'description' => __( 'Tool return message.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Prepare links for the request. + * + * @param string $id + * @return array + */ + protected function prepare_links( $id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'item' => array( + 'href' => rest_url( trailingslashit( $base ) . $id ), + 'embeddable' => true, + ), + ); + + return $links; + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } + + /** + * Actually executes a a tool. + * + * @param string $tool + * @return array + */ + public function execute_tool( $tool ) { + global $wpdb; + $ran = true; + switch ( $tool ) { + case 'clear_transients' : + wc_delete_product_transients(); + wc_delete_shop_order_transients(); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + $message = __( 'Product transients cleared', 'woocommerce' ); + break; + case 'clear_expired_transients' : + /* + * Deletes all expired transients. The multi-table delete syntax is used. + * to delete the transient record from table a, and the corresponding. + * transient_timeout record from table b. + * + * Based on code inside core's upgrade_network() function. + */ + $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b + WHERE a.option_name LIKE %s + AND a.option_name NOT LIKE %s + AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) + AND b.option_value < %d"; + $rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) ); + + $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b + WHERE a.option_name LIKE %s + AND a.option_name NOT LIKE %s + AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) ) + AND b.option_value < %d"; + $rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) ); + + $message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), $rows + $rows2 ); + break; + case 'delete_orphaned_variations' : + /** + * Delete orphans + */ + $result = absint( $wpdb->query( "DELETE products + FROM {$wpdb->posts} products + LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent + WHERE wp.ID IS NULL AND products.post_type = 'product_variation';" ) ); + $message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result ); + break; + case 'add_order_indexes' : + /** + * Add billing and shipping address indexes containing the customer name for orders + * that don't have address indexes yet. + */ + $sql = "INSERT INTO {$wpdb->postmeta}( post_id, meta_key, meta_value ) + SELECT post_id, '%1\$s', GROUP_CONCAT( meta_value SEPARATOR ' ' ) + FROM {$wpdb->postmeta} + WHERE meta_key IN ( '%2\$s', '%3\$s' ) + AND post_id IN ( SELECT DISTINCT post_id FROM {$wpdb->postmeta} + WHERE post_id NOT IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='%1\$s' ) + AND post_id IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='%3\$s' ) ) + GROUP BY post_id"; + $rows = $wpdb->query( $wpdb->prepare( $sql, '_billing_address_index', '_billing_first_name', '_billing_last_name' ) ); + $rows += $wpdb->query( $wpdb->prepare( $sql, '_shipping_address_index', '_shipping_first_name', '_shipping_last_name' ) ); + + $message = sprintf( __( '%d indexes added', 'woocommerce' ), $rows ); + break; + case 'reset_roles' : + // Remove then re-add caps and roles + WC_Install::remove_roles(); + WC_Install::create_roles(); + $message = __( 'Roles successfully reset', 'woocommerce' ); + break; + case 'recount_terms' : + $product_cats = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'id=>parent' ) ); + _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false ); + $product_tags = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'id=>parent' ) ); + _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false ); + $message = __( 'Terms successfully recounted', 'woocommerce' ); + break; + case 'clear_sessions' : + $wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" ); + wp_cache_flush(); + $message = __( 'Sessions successfully cleared', 'woocommerce' ); + break; + case 'install_pages' : + WC_Install::create_pages(); + $message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' ); + break; + case 'delete_taxes' : + + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" ); + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" ); + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + $message = __( 'Tax rates successfully deleted', 'woocommerce' ); + break; + case 'reset_tracking' : + delete_option( 'woocommerce_allow_tracking' ); + WC_Admin_Notices::add_notice( 'tracking' ); + $message = __( 'Usage tracking settings successfully reset.', 'woocommerce' ); + break; + default : + $tools = $this->get_tools(); + if ( isset( $tools[ $tool ]['callback'] ) ) { + $callback = $tools[ $tool ]['callback']; + $return = call_user_func( $callback ); + if ( is_string( $return ) ) { + $message = $return; + } elseif ( false === $return ) { + $callback_string = is_array( $callback ) ? get_class( $callback[0] ) . '::' . $callback[1] : $callback; + $ran = false; + $message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string ); + } else { + $message = __( 'Tool ran.', 'woocommerce' ); + } + } else { + $ran = false; + $message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' ); + } + break; + } + + return array( 'success' => $ran, 'message' => $message ); + } +} diff --git a/includes/api/class-wc-rest-tax-classes-controller.php b/includes/api/class-wc-rest-tax-classes-controller.php new file mode 100644 index 00000000000..7c454b53e0f --- /dev/null +++ b/includes/api/class-wc-rest-tax-classes-controller.php @@ -0,0 +1,31 @@ +/deliveries endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Webhook Deliveries controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Webhook_Deliveries_V1_Controller + */ +class WC_REST_Webhook_Deliveries_Controller extends WC_REST_Webhook_Deliveries_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Prepare a single webhook delivery output for response. + * + * @param stdClass $log Delivery log object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $log, $request ) { + $data = (array) $log; + + // Add timestamp. + $data['date_created'] = wc_rest_prepare_date_response( $log->comment->comment_date ); + $data['date_created_gmt'] = wc_rest_prepare_date_response( $log->comment->comment_date_gmt ); + + // Remove comment object. + unset( $data['comment'] ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $log ) ); + + /** + * Filter webhook delivery object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $log Delivery log object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook_delivery', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'duration' => array( + 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'summary' => array( + 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_url' => array( + 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_headers' => array( + 'description' => __( 'Request headers.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'request_body' => array( + 'description' => __( 'Request body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_code' => array( + 'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_message' => array( + 'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_headers' => array( + 'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'response_body' => array( + 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the webhook delivery was logged, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/class-wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php new file mode 100644 index 00000000000..672299bdbe2 --- /dev/null +++ b/includes/api/class-wc-rest-webhooks-controller.php @@ -0,0 +1,186 @@ +ID; + $webhook = new WC_Webhook( $id ); + $data = array( + 'id' => $webhook->id, + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'date_created' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $post, $request ) ); + + /** + * Filter webhook object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Webhook $webhook Webhook object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); + } + + /** + * Get the default REST API version. + * + * @since 3.0.0 + * @return string + */ + protected function get_default_api_version() { + return 'wp_api_v2'; + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Webhook status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'active', + 'enum' => array( 'active', 'paused', 'disabled' ), + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wc_is_webhook_valid_topic', + ), + ), + 'topic' => array( + 'description' => __( 'Webhook topic.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'resource' => array( + 'description' => __( 'Webhook resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'event' => array( + 'description' => __( 'Webhook event.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'hooks' => array( + 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'delivery_url' => array( + 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'secret' => array( + 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default is a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the webhook was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the webhook was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/interface-wc-api-handler.php b/includes/api/interface-wc-api-handler.php deleted file mode 100644 index a4f8edc6412..00000000000 --- a/includes/api/interface-wc-api-handler.php +++ /dev/null @@ -1,45 +0,0 @@ -ID ); + $data = $coupon->get_data(); + + $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); + $format_date = array( 'date_created', 'date_modified', 'date_expires' ); + $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : null; + } + + // Format null values. + foreach ( $format_null as $key ) { + $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $post, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare a single coupon for create or update. + * + * @deprecated 3.0.0 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + global $wpdb; + + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $coupon = new WC_Coupon( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) { + if ( empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + } + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'code' : + $coupon_code = wc_format_coupon_code( $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon->set_code( $coupon_code ); + break; + case 'meta_data' : + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + case 'description' : + $coupon->set_description( wp_filter_post_kses( $value ) ); + break; + default : + if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { + $coupon->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param WC_Coupon $coupon The coupon object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request ); + } +} diff --git a/includes/api/legacy/class-wc-rest-legacy-orders-controller.php b/includes/api/legacy/class-wc-rest-legacy-orders-controller.php new file mode 100644 index 00000000000..5b4e501c66d --- /dev/null +++ b/includes/api/legacy/class-wc-rest-legacy-orders-controller.php @@ -0,0 +1,300 @@ + '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $request['product'] ) ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + return $args; + } + + /** + * Prepare a single order output for response. + * + * @deprecated 3.0 + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + $this->request = $request; + $statuses = wc_get_order_statuses(); + $order = wc_get_order( $post ); + $data = array_merge( array( 'id' => $order->get_id() ), $order->get_data() ); + $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); + $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); + $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : false; + } + + // Format the order status. + $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; + + // Format line items. + foreach ( $format_line_items as $key ) { + $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); + } + + // Refunds. + $data['refunds'] = array(); + foreach ( $order->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'refund' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $order, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare a single order for create. + * + * @deprecated 3.0 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WC_Order $data Object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'billing' : + case 'shipping' : + $this->update_address( $order, $value, $key ); + break; + case 'line_items' : + case 'shipping_lines' : + case 'fee_lines' : + case 'coupon_lines' : + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data' : + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default : + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the data for the insert. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WC_Order $order The prder object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); + } + + /** + * Create base WC Order object. + * + * @deprecated 3.0.0 + * + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * Create order. + * + * @deprecated 3.0.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + try { + // Make sure customer exists. + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $order = $this->prepare_item_for_database( $request ); + $order->set_created_via( 'rest-api' ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->calculate_totals(); + $order->save(); + + // Handle set paid. + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update order. + * + * @deprecated 3.0.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function update_order( $request ) { + try { + $order = $this->prepare_item_for_database( $request ); + $order->save(); + + // Handle set paid. + if ( $order->needs_payment() && true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $order->calculate_totals(); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/class-wc-rest-legacy-products-controller.php b/includes/api/legacy/class-wc-rest-legacy-products-controller.php new file mode 100644 index 00000000000..3539febc6fc --- /dev/null +++ b/includes/api/legacy/class-wc-rest-legacy-products-controller.php @@ -0,0 +1,803 @@ + 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); + } + + // Filter product in stock or out of stock. + if ( is_bool( $request['in_stock'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_stock_status', + 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', + ) ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $args[ $on_sale_key ] += wc_get_product_ids_on_sale(); + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + return $args; + } + + /** + * Prepare a single product output for response. + * + * @deprecated 3.0.0 + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $post, $request ) { + $product = wc_get_product( $post ); + $data = $this->get_product_data( $product ); + + // Add variations to variable products. + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $data['variations'] = $product->get_children(); + } + + // Add grouped products data. + if ( $product->is_type( 'grouped' ) && $product->has_child() ) { + $data['grouped_products'] = $product->get_children(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $product, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Get product menu order. + * + * @deprecated 3.0.0 + * @param WC_Product $product Product instance. + * @return int + */ + protected function get_product_menu_order( $product ) { + return $product->get_menu_order(); + } + + /** + * Save product meta. + * + * @deprecated 3.0.0 + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_product_meta( $product, $request ) { + $product = $this->set_product_meta( $product, $request ); + $product->save(); + + return true; + } + + /** + * Set product meta. + * + * @deprecated 3.0.0 + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return WC_Product + */ + protected function set_product_meta( $product, $request ) { + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + } + + // Product parent ID for groups. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + return $product; + } + + /** + * Save variations. + * + * @deprecated 3.0.0 + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return bool + */ + protected function save_variations_data( $product, $request ) { + foreach ( $request['variations'] as $menu_order => $data ) { + $variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 ); + + // Create initial name and status. + if ( ! $variation->get_slug() ) { + /* translators: 1: variation id 2: product name */ + $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); + $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); + } + + // Parent ID. + $variation->set_parent_id( $product->get_id() ); + + // Menu order. + $variation->set_menu_order( $menu_order ); + + // Status. + if ( isset( $data['visible'] ) ) { + $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { + $image = $data['image']; + $image = current( $image ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->set_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $variation->set_downloadable( $data['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + if ( isset( $data['manage_stock'] ) ) { + $variation->set_manage_stock( $data['manage_stock'] ); + } + + if ( isset( $data['in_stock'] ) ) { + $variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' ); + } + + if ( isset( $data['backorders'] ) ) { + $variation->set_backorders( $data['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $data['date_on_sale_from'] ); + } + + if ( isset( $data['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $data['date_on_sale_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Description. + if ( isset( $data['description'] ) ) { + $variation->set_description( wp_kses_post( $data['description'] ) ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + $parent_attributes = $product->get_attributes(); + + foreach ( $data['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data ); + } + + return true; + } + + /** + * Add post meta fields. + * + * @deprecated 3.0.0 + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request data. + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return $this->update_post_meta_fields( $post, $request ); + } + + /** + * Update post meta fields. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request data. + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $product = $this->set_product_meta( $product, $request ); + + // Save the product data. + $product->save(); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations_data( $product, $request ); + } + } + + // Clear caches here so in sync with any new variations/children. + wc_delete_product_transients( $product->get_id() ); + wp_cache_delete( 'product-' . $product->get_id(), 'products' ); + + return true; + } + + /** + * Delete post. + * + * @deprecated 3.0.0 + * + * @param int|WP_Post $id Post ID or WP_Post instance. + */ + protected function delete_post( $id ) { + if ( ! empty( $id->ID ) ) { + $id = $id->ID; + } elseif ( ! is_numeric( $id ) || 0 >= $id ) { + return; + } + + // Delete product attachments. + $attachments = get_posts( array( + 'post_parent' => $id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product. + $product = wc_get_product( $id ); + $product->delete( true ); + } + + /** + * Get post types. + * + * @deprecated 3.0.0 + * + * @return array + */ + protected function get_post_types() { + return array( 'product', 'product_variation' ); + } + + /** + * Save product images. + * + * @deprecated 3.0.0 + * + * @param int $product_id + * @param array $images + * @throws WC_REST_Exception + */ + protected function save_product_images( $product_id, $images ) { + $product = wc_get_product( $product_id ); + + return set_product_images( $product, $images ); + } +} diff --git a/includes/api/legacy/v1/class-wc-api-authentication.php b/includes/api/legacy/v1/class-wc-api-authentication.php new file mode 100644 index 00000000000..ba9372d993a --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-authentication.php @@ -0,0 +1,411 @@ +api->server->path ) { + return new WP_User( 0 ); + } + + try { + + if ( is_ssl() ) { + $keys = $this->perform_ssl_authentication(); + } else { + $keys = $this->perform_oauth_authentication(); + } + + // Check API key-specific permission + $this->check_api_key_permissions( $keys['permissions'] ); + + $user = $this->get_user_by_id( $keys['user_id'] ); + + $this->update_api_key_last_access( $keys['key_id'] ); + + } catch ( Exception $e ) { + $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + return $user; + } + + /** + * SSL-encrypted requests are not subject to sniffing or man-in-the-middle + * attacks, so the request can be authenticated by simply looking up the user + * associated with the given consumer key and confirming the consumer secret + * provided is valid + * + * @since 2.1 + * @return array + * @throws Exception + */ + private function perform_ssl_authentication() { + + $params = WC()->api->server->params['GET']; + + // Get consumer key + if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) { + + // Should be in HTTP Auth header by default + $consumer_key = $_SERVER['PHP_AUTH_USER']; + + } elseif ( ! empty( $params['consumer_key'] ) ) { + + // Allow a query string parameter as a fallback + $consumer_key = $params['consumer_key']; + + } else { + + throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 ); + } + + // Get consumer secret + if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { + + // Should be in HTTP Auth header by default + $consumer_secret = $_SERVER['PHP_AUTH_PW']; + + } elseif ( ! empty( $params['consumer_secret'] ) ) { + + // Allow a query string parameter as a fallback + $consumer_secret = $params['consumer_secret']; + + } else { + + throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 ); + } + + $keys = $this->get_keys_by_consumer_key( $consumer_key ); + + if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) { + throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 ); + } + + return $keys; + } + + /** + * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests + * + * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP + * + * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: + * + * 1) There is no token associated with request/responses, only consumer keys/secrets are used + * + * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, + * This is because there is no cross-OS function within PHP to get the raw Authorization header + * + * @link http://tools.ietf.org/html/rfc5849 for the full spec + * @since 2.1 + * @return array + * @throws Exception + */ + private function perform_oauth_authentication() { + + $params = WC()->api->server->params['GET']; + + $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); + + // Check for required OAuth parameters + foreach ( $param_names as $param_name ) { + + if ( empty( $params[ $param_name ] ) ) { + /* translators: %s: parameter name */ + throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 ); + } + } + + // Fetch WP user by consumer key + $keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] ); + + // Perform OAuth validation + $this->check_oauth_signature( $keys, $params ); + $this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] ); + + // Authentication successful, return user + return $keys; + } + + /** + * Return the keys for the given consumer key + * + * @since 2.4.0 + * @param string $consumer_key + * @return array + * @throws Exception + */ + private function get_keys_by_consumer_key( $consumer_key ) { + global $wpdb; + + $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); + + $keys = $wpdb->get_row( $wpdb->prepare( " + SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces + FROM {$wpdb->prefix}woocommerce_api_keys + WHERE consumer_key = '%s' + ", $consumer_key ), ARRAY_A ); + + if ( empty( $keys ) ) { + throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 ); + } + + return $keys; + } + + /** + * Get user by ID + * + * @since 2.4.0 + * @param int $user_id + * @return WP_User + * + * @throws Exception + */ + private function get_user_by_id( $user_id ) { + $user = get_user_by( 'id', $user_id ); + + if ( ! $user ) { + throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 ); + } + + return $user; + } + + /** + * Check if the consumer secret provided for the given user is valid + * + * @since 2.1 + * @param string $keys_consumer_secret + * @param string $consumer_secret + * @return bool + */ + private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) { + return hash_equals( $keys_consumer_secret, $consumer_secret ); + } + + /** + * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer + * has a valid key/secret + * + * @param array $keys + * @param array $params the request parameters + * @throws Exception + */ + private function check_oauth_signature( $keys, $params ) { + + $http_method = strtoupper( WC()->api->server->method ); + + $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path ); + + // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature + $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); + unset( $params['oauth_signature'] ); + + // Remove filters and convert them from array to strings to void normalize issues + if ( isset( $params['filter'] ) ) { + $filters = $params['filter']; + unset( $params['filter'] ); + foreach ( $filters as $filter => $filter_value ) { + $params[ 'filter[' . $filter . ']' ] = $filter_value; + } + } + + // Normalize parameter key/values + $params = $this->normalize_parameters( $params ); + + // Sort parameters + if ( ! uksort( $params, 'strcmp' ) ) { + throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 ); + } + + // Form query string + $query_params = array(); + foreach ( $params as $param_key => $param_value ) { + + $query_params[] = $param_key . '%3D' . $param_value; // join with equals sign + } + $query_string = implode( '%26', $query_params ); // join with ampersand + + $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; + + if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { + throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 ); + } + + $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); + + $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) ); + + if ( ! hash_equals( $signature, $consumer_signature ) ) { + throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 ); + } + } + + /** + * Normalize each parameter by assuming each parameter may have already been + * encoded, so attempt to decode, and then re-encode according to RFC 3986 + * + * Note both the key and value is normalized so a filter param like: + * + * 'filter[period]' => 'week' + * + * is encoded to: + * + * 'filter%5Bperiod%5D' => 'week' + * + * This conforms to the OAuth 1.0a spec which indicates the entire query string + * should be URL encoded + * + * @since 2.1 + * @see rawurlencode() + * @param array $parameters un-normalized pararmeters + * @return array normalized parameters + */ + private function normalize_parameters( $parameters ) { + + $normalized_parameters = array(); + + foreach ( $parameters as $key => $value ) { + + // Percent symbols (%) must be double-encoded + $key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) ); + $value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); + + $normalized_parameters[ $key ] = $value; + } + + return $normalized_parameters; + } + + /** + * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where + * an attacker could attempt to re-send an intercepted request at a later time. + * + * - A timestamp is valid if it is within 15 minutes of now + * - A nonce is valid if it has not been used within the last 15 minutes + * + * @param array $keys + * @param int $timestamp the unix timestamp for when the request was made + * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated + * @throws Exception + */ + private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) { + global $wpdb; + + $valid_window = 15 * 60; // 15 minute window + + if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { + throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ) ); + } + + $used_nonces = maybe_unserialize( $keys['nonces'] ); + + if ( empty( $used_nonces ) ) { + $used_nonces = array(); + } + + if ( in_array( $nonce, $used_nonces ) ) { + throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 ); + } + + $used_nonces[ $timestamp ] = $nonce; + + // Remove expired nonces + foreach ( $used_nonces as $nonce_timestamp => $nonce ) { + if ( $nonce_timestamp < ( time() - $valid_window ) ) { + unset( $used_nonces[ $nonce_timestamp ] ); + } + } + + $used_nonces = maybe_serialize( $used_nonces ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'nonces' => $used_nonces ), + array( 'key_id' => $keys['key_id'] ), + array( '%s' ), + array( '%d' ) + ); + } + + /** + * Check that the API keys provided have the proper key-specific permissions to either read or write API resources + * + * @param string $key_permissions + * @throws Exception if the permission check fails + */ + public function check_api_key_permissions( $key_permissions ) { + switch ( WC()->api->server->method ) { + + case 'HEAD': + case 'GET': + if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) { + throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 ); + } + break; + + case 'POST': + case 'PUT': + case 'PATCH': + case 'DELETE': + if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) { + throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 ); + } + break; + } + } + + /** + * Updated API Key last access datetime + * + * @since 2.4.0 + * + * @param int $key_id + */ + private function update_api_key_last_access( $key_id ) { + global $wpdb; + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'last_access' => current_time( 'mysql' ) ), + array( 'key_id' => $key_id ), + array( '%s' ), + array( '%d' ) + ); + } +} diff --git a/includes/api/legacy/v1/class-wc-api-coupons.php b/includes/api/legacy/v1/class-wc-api-coupons.php new file mode 100644 index 00000000000..244b5efd3b0 --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-coupons.php @@ -0,0 +1,247 @@ + + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET /coupons + $routes[ $this->base ] = array( + array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ), + ); + + # GET /coupons/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ), + ); + + # GET /coupons/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), + ); + + # GET /coupons/code/, note that coupon codes can contain spaces, dashes and underscores + $routes[ $this->base . '/code/(?P\w[\w\s\-]*)' ] = array( + array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ), + ); + + return $routes; + } + + /** + * Get all coupons + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param int $page + * @return array + */ + public function get_coupons( $fields = null, $filter = array(), $page = 1 ) { + + $filter['page'] = $page; + + $query = $this->query_coupons( $filter ); + + $coupons = array(); + + foreach ( $query->posts as $coupon_id ) { + + if ( ! $this->is_readable( $coupon_id ) ) { + continue; + } + + $coupons[] = current( $this->get_coupon( $coupon_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'coupons' => $coupons ); + } + + /** + * Get the coupon for the given ID + * + * @since 2.1 + * + * @param int $id the coupon ID + * @param string $fields fields to include in response + * + * @return array|WP_Error + * @throws WC_API_Exception + */ + public function get_coupon( $id, $fields = null ) { + $id = $this->validate_request( $id, 'shop_coupon', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $coupon = new WC_Coupon( $id ); + + if ( 0 === $coupon->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 ); + } + + $coupon_data = array( + 'id' => $coupon->get_id(), + 'code' => $coupon->get_code(), + 'type' => $coupon->get_discount_type(), + 'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times. + 'amount' => wc_format_decimal( $coupon->get_amount(), 2 ), + 'individual_use' => $coupon->get_individual_use(), + 'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ), + 'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ), + 'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null, + 'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null, + 'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(), + 'usage_count' => (int) $coupon->get_usage_count(), + 'expiry_date' => $this->server->format_datetime( $coupon->get_date_expires() ? $coupon->get_date_expires()->getTimestamp() : 0 ), // API gives UTC times. + 'enable_free_shipping' => $coupon->get_free_shipping(), + 'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ), + 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ), + 'exclude_sale_items' => $coupon->get_exclude_sale_items(), + 'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ), + 'customer_emails' => $coupon->get_email_restrictions(), + ); + + return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) ); + } + + /** + * Get the total number of coupons + * + * @since 2.1 + * + * @param array $filter + * + * @return array|WP_Error + */ + public function get_coupons_count( $filter = array() ) { + + $query = $this->query_coupons( $filter ); + + if ( ! current_user_can( 'read_private_shop_coupons' ) ) { + return new WP_Error( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), array( 'status' => 401 ) ); + } + + return array( 'count' => (int) $query->found_posts ); + } + + /** + * Get the coupon for the given code + * + * @since 2.1 + * @param string $code the coupon code + * @param string $fields fields to include in response + * @return int|WP_Error + */ + public function get_coupon_by_code( $code, $fields = null ) { + global $wpdb; + + $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) ); + + if ( is_null( $id ) ) { + return new WP_Error( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $this->get_coupon( $id, $fields ); + } + + /** + * Create a coupon + * + * @param array $data + * @return array + */ + public function create_coupon( $data ) { + + return array(); + } + + /** + * Edit a coupon + * + * @param int $id the coupon ID + * @param array $data + * @return array|WP_Error + */ + public function edit_coupon( $id, $data ) { + + $id = $this->validate_request( $id, 'shop_coupon', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + return $this->get_coupon( $id ); + } + + /** + * Delete a coupon + * + * @param int $id the coupon ID + * @param bool $force true to permanently delete coupon, false to move to trash + * @return array|WP_Error + */ + public function delete_coupon( $id, $force = false ) { + + $id = $this->validate_request( $id, 'shop_coupon', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) ); + } + + /** + * Helper method to get coupon post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_coupons( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'shop_coupon', + 'post_status' => 'publish', + ); + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } +} diff --git a/includes/api/legacy/v1/class-wc-api-customers.php b/includes/api/legacy/v1/class-wc-api-customers.php new file mode 100644 index 00000000000..d1b8fac6272 --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-customers.php @@ -0,0 +1,482 @@ + + * GET /customers//orders + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET /customers + $routes[ $this->base ] = array( + array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), + ); + + return $routes; + } + + /** + * Get all customers + * + * @since 2.1 + * @param array $fields + * @param array $filter + * @param int $page + * @return array + */ + public function get_customers( $fields = null, $filter = array(), $page = 1 ) { + + $filter['page'] = $page; + + $query = $this->query_customers( $filter ); + + $customers = array(); + + foreach ( $query->get_results() as $user_id ) { + + if ( ! $this->is_readable( $user_id ) ) { + continue; + } + + $customers[] = current( $this->get_customer( $user_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'customers' => $customers ); + } + + /** + * Get the customer for the given ID + * + * @since 2.1 + * @param int $id the customer ID + * @param string $fields + * @return array|WP_Error + */ + public function get_customer( $id, $fields = null ) { + global $wpdb; + + $id = $this->validate_request( $id, 'customer', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $customer = new WC_Customer( $id ); + $last_order = $customer->get_last_order(); + $customer_data = array( + 'id' => $customer->get_id(), + 'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. + 'email' => $customer->get_email(), + 'first_name' => $customer->get_first_name(), + 'last_name' => $customer->get_last_name(), + 'username' => $customer->get_username(), + 'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null, + 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times. + 'orders_count' => $customer->get_order_count(), + 'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ), + 'avatar_url' => $customer->get_avatar_url(), + 'billing_address' => array( + 'first_name' => $customer->get_billing_first_name(), + 'last_name' => $customer->get_billing_last_name(), + 'company' => $customer->get_billing_company(), + 'address_1' => $customer->get_billing_address_1(), + 'address_2' => $customer->get_billing_address_2(), + 'city' => $customer->get_billing_city(), + 'state' => $customer->get_billing_state(), + 'postcode' => $customer->get_billing_postcode(), + 'country' => $customer->get_billing_country(), + 'email' => $customer->get_billing_email(), + 'phone' => $customer->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $customer->get_shipping_first_name(), + 'last_name' => $customer->get_shipping_last_name(), + 'company' => $customer->get_shipping_company(), + 'address_1' => $customer->get_shipping_address_1(), + 'address_2' => $customer->get_shipping_address_2(), + 'city' => $customer->get_shipping_city(), + 'state' => $customer->get_shipping_state(), + 'postcode' => $customer->get_shipping_postcode(), + 'country' => $customer->get_shipping_country(), + ), + ); + + return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) ); + } + + /** + * Get the total number of customers + * + * @since 2.1 + * @param array $filter + * @return array|WP_Error + */ + public function get_customers_count( $filter = array() ) { + + $query = $this->query_customers( $filter ); + + if ( ! current_user_can( 'list_users' ) ) { + return new WP_Error( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), array( 'status' => 401 ) ); + } + + return array( 'count' => count( $query->get_results() ) ); + } + + + /** + * Create a customer + * + * @param array $data + * @return array|WP_Error + */ + public function create_customer( $data ) { + + if ( ! current_user_can( 'create_users' ) ) { + return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) ); + } + + return array(); + } + + /** + * Edit a customer + * + * @param int $id the customer ID + * @param array $data + * @return array|WP_Error + */ + public function edit_customer( $id, $data ) { + + $id = $this->validate_request( $id, 'customer', 'edit' ); + + if ( ! is_wp_error( $id ) ) { + return $id; + } + + return $this->get_customer( $id ); + } + + /** + * Delete a customer + * + * @param int $id the customer ID + * @return array|WP_Error + */ + public function delete_customer( $id ) { + + $id = $this->validate_request( $id, 'customer', 'delete' ); + + if ( ! is_wp_error( $id ) ) { + return $id; + } + + return $this->delete( $id, 'customer' ); + } + + /** + * Get the orders for a customer + * + * @since 2.1 + * @param int $id the customer ID + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_customer_orders( $id, $fields = null ) { + global $wpdb; + + $id = $this->validate_request( $id, 'customer', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order_ids = wc_get_orders( array( + 'customer' => $id, + 'limit' => -1, + 'orderby' => 'date', + 'order' => 'ASC', + 'return' => 'ids', + ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $orders = array(); + + foreach ( $order_ids as $order_id ) { + $orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) ); + } + + return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) ); + } + + /** + * Helper method to get customer user objects + * + * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited + * pagination support + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_User_Query + */ + private function query_customers( $args = array() ) { + + // default users per page + $users_per_page = get_option( 'posts_per_page' ); + + // set base query arguments + $query_args = array( + 'fields' => 'ID', + 'role' => 'customer', + 'orderby' => 'registered', + 'number' => $users_per_page, + ); + + // search + if ( ! empty( $args['q'] ) ) { + $query_args['search'] = $args['q']; + } + + // limit number of users returned + if ( ! empty( $args['limit'] ) ) { + + $query_args['number'] = absint( $args['limit'] ); + + $users_per_page = absint( $args['limit'] ); + } + + // page + $page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1; + + // offset + if ( ! empty( $args['offset'] ) ) { + $query_args['offset'] = absint( $args['offset'] ); + } else { + $query_args['offset'] = $users_per_page * ( $page - 1 ); + } + + // created date + if ( ! empty( $args['created_at_min'] ) ) { + $this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] ); + } + + if ( ! empty( $args['created_at_max'] ) ) { + $this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] ); + } + + $query = new WP_User_Query( $query_args ); + + // helper members for pagination headers + $query->total_pages = ceil( $query->get_total() / $users_per_page ); + $query->page = $page; + + return $query; + } + + /** + * Add customer data to orders + * + * @since 2.1 + * @param $order_data + * @param $order + * @return array + */ + public function add_customer_data( $order_data, $order ) { + + if ( 0 == $order->get_user_id() ) { + + // add customer data from order + $order_data['customer'] = array( + 'id' => 0, + 'email' => $order->get_billing_email(), + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + ); + + } else { + + $order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) ); + } + + return $order_data; + } + + /** + * Modify the WP_User_Query to support filtering on the date the customer was created + * + * @since 2.1 + * @param WP_User_Query $query + */ + public function modify_user_query( $query ) { + + if ( $this->created_at_min ) { + $query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) ); + } + + if ( $this->created_at_max ) { + $query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) ); + } + } + + /** + * Validate the request by checking: + * + * 1) the ID is a valid integer + * 2) the ID returns a valid WP_User + * 3) the current user has the proper permissions + * + * @since 2.1 + * @see WC_API_Resource::validate_request() + * @param string|int $id the customer ID + * @param string $type the request type, unused because this method overrides the parent class + * @param string $context the context of the request, either `read`, `edit` or `delete` + * @return int|WP_Error valid user ID or WP_Error if any of the checks fails + */ + protected function validate_request( $id, $type, $context ) { + + $id = absint( $id ); + + // validate ID + if ( empty( $id ) ) { + return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // non-existent IDs return a valid WP_User object with the user ID = 0 + $customer = new WP_User( $id ); + + if ( 0 === $customer->ID ) { + return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // validate permissions + switch ( $context ) { + + case 'read': + if ( ! current_user_can( 'list_users' ) ) { + return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) ); + } + break; + + case 'edit': + if ( ! current_user_can( 'edit_users' ) ) { + return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) ); + } + break; + + case 'delete': + if ( ! current_user_can( 'delete_users' ) ) { + return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) ); + } + break; + } + + return $id; + } + + /** + * Check if the current user can read users + * + * @since 2.1 + * @see WC_API_Resource::is_readable() + * @param int|WP_Post $post unused + * @return bool true if the current user can read users, false otherwise + */ + protected function is_readable( $post ) { + + return current_user_can( 'list_users' ); + } +} diff --git a/includes/api/legacy/v1/class-wc-api-json-handler.php b/includes/api/legacy/v1/class-wc-api-json-handler.php new file mode 100644 index 00000000000..adc51290673 --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-json-handler.php @@ -0,0 +1,79 @@ +api->server->send_status( 400 ); + + $data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ); + } + + // Check for invalid characters (only alphanumeric allowed) + if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) { + + WC()->api->server->send_status( 400 ); + + $data = array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ); + } + + // see http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ + WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); + + // Prepend '/**/' to mitigate possible JSONP Flash attacks + return '/**/' . $_GET['_jsonp'] . '(' . json_encode( $data ) . ')'; + } + + return json_encode( $data ); + } +} diff --git a/includes/api/legacy/v1/class-wc-api-orders.php b/includes/api/legacy/v1/class-wc-api-orders.php new file mode 100644 index 00000000000..2129f832eea --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-orders.php @@ -0,0 +1,396 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET|PUT /orders/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_order' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + ); + + return $routes; + } + + /** + * Get all orders + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param string $status + * @param int $page + * @return array + */ + public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_orders( $filter ); + + $orders = array(); + + foreach ( $query->posts as $order_id ) { + + if ( ! $this->is_readable( $order_id ) ) { + continue; + } + + $orders[] = current( $this->get_order( $order_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'orders' => $orders ); + } + + + /** + * Get the order for the given ID + * + * @since 2.1 + * @param int $id the order ID + * @param array $fields + * @return array|WP_Error + */ + public function get_order( $id, $fields = null ) { + + // ensure order ID is valid & user has permission to read + $id = $this->validate_request( $id, 'shop_order', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order = wc_get_order( $id ); + $order_data = array( + 'id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'status' => $order->get_status(), + 'currency' => $order->get_currency(), + 'total' => wc_format_decimal( $order->get_total(), 2 ), + 'subtotal' => wc_format_decimal( $this->get_order_subtotal( $order ), 2 ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), 2 ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), 2 ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), 2 ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), 2 ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), 2 ), + 'cart_discount' => wc_format_decimal( 0, 2 ), + 'order_discount' => wc_format_decimal( 0, 2 ), + 'shipping_methods' => $order->get_shipping_method(), + 'payment_details' => array( + 'method_id' => $order->get_payment_method(), + 'method_title' => $order->get_payment_method_title(), + 'paid' => ! is_null( $order->get_date_paid() ), + ), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + 'note' => $order->get_customer_note(), + 'customer_ip' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'customer_id' => $order->get_user_id(), + 'view_order_url' => $order->get_view_order_url(), + 'line_items' => array(), + 'shipping_lines' => array(), + 'tax_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // add line items + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $order_data['line_items'][] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), + 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + ); + } + + // add shipping + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $order_data['shipping_lines'][] = array( + 'id' => $shipping_item_id, + 'method_id' => $shipping_item->get_method_id(), + 'method_title' => $shipping_item->get_name(), + 'total' => wc_format_decimal( $shipping_item->get_total(), 2 ), + ); + } + + // add taxes + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $order_data['tax_lines'][] = array( + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, 2 ), + 'compound' => (bool) $tax->is_compound, + ); + } + + // add fees + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $order_data['fee_lines'][] = array( + 'id' => $fee_item_id, + 'title' => $fee_item->get_name(), + 'tax_class' => $fee_item->get_tax_class(), + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), 2 ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), 2 ), + ); + } + + // add coupons + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $order_data['coupon_lines'][] = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item->get_code(), + 'amount' => wc_format_decimal( $coupon_item->get_discount(), 2 ), + ); + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.1 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $query = $this->query_orders( $filter ); + + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + return new WP_Error( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), array( 'status' => 401 ) ); + } + + return array( 'count' => (int) $query->found_posts ); + } + + /** + * Edit an order + * + * API v1 only allows updating the status of an order + * + * @since 2.1 + * @param int $id the order ID + * @param array $data + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + + $id = $this->validate_request( $id, 'shop_order', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order = wc_get_order( $id ); + + if ( ! empty( $data['status'] ) ) { + + $order->update_status( $data['status'], isset( $data['note'] ) ? $data['note'] : '' ); + } + + return $this->get_order( $id ); + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, 'shop_order', 'delete' ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param int $id the order ID + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $id, $fields = null ) { + + // ensure ID is valid order ID + $id = $this->validate_request( $id, 'shop_order', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $args = array( + 'post_id' => $id, + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $order_notes = array(); + + foreach ( $notes as $note ) { + + $order_notes[] = array( + 'id' => $note->comment_ID, + 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false, + ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $id, $fields, $notes, $this->server ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'shop_order', + 'post_status' => array_keys( wc_get_order_statuses() ), + ); + + // add status argument + if ( ! empty( $args['status'] ) ) { + + $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); + $statuses = explode( ',', $statuses ); + $query_args['post_status'] = $statuses; + + unset( $args['status'] ); + + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Helper method to get the order subtotal + * + * @since 2.1 + * @param WC_Order $order + * @return float + */ + private function get_order_subtotal( $order ) { + $subtotal = 0; + + // subtotal + foreach ( $order->get_items() as $item ) { + $subtotal += $item->get_subtotal(); + } + + return $subtotal; + } +} diff --git a/includes/api/legacy/v1/class-wc-api-products.php b/includes/api/legacy/v1/class-wc-api-products.php new file mode 100644 index 00000000000..de579991255 --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-products.php @@ -0,0 +1,544 @@ + + * GET /products//reviews + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET /products + $routes[ $this->base ] = array( + array( array( $this, 'get_products' ), WC_API_Server::READABLE ), + ); + + # GET /products/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), + ); + + # GET /products/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_product' ), WC_API_Server::READABLE ), + ); + + # GET /products//reviews + $routes[ $this->base . '/(?P\d+)/reviews' ] = array( + array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), + ); + + return $routes; + } + + /** + * Get all products + * + * @since 2.1 + * @param string $fields + * @param string $type + * @param array $filter + * @param int $page + * @return array + */ + public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $filter['page'] = $page; + + $query = $this->query_products( $filter ); + + $products = array(); + + foreach ( $query->posts as $product_id ) { + + if ( ! $this->is_readable( $product_id ) ) { + continue; + } + + $products[] = current( $this->get_product( $product_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'products' => $products ); + } + + /** + * Get the product for the given ID + * + * @since 2.1 + * @param int $id the product ID + * @param string $fields + * @return array|WP_Error + */ + public function get_product( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + // add data that applies to every product type + $product_data = $this->get_product_data( $product ); + + // add variations to variable products + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $product_data['variations'] = $this->get_variation_data( $product ); + } + + // add the parent product data to an individual variation + if ( $product->is_type( 'variation' ) ) { + $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); + } + + return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.1 + * + * @param string $type + * @param array $filter + * + * @return array|WP_Error + */ + public function get_products_count( $type = null, $filter = array() ) { + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + if ( ! current_user_can( 'read_private_products' ) ) { + return new WP_Error( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $query = $this->query_products( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } + + /** + * Edit a product + * + * @param int $id the product ID + * @param array $data + * @return array|WP_Error + */ + public function edit_product( $id, $data ) { + + $id = $this->validate_request( $id, 'product', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + return $this->get_product( $id ); + } + + /** + * Delete a product + * + * @param int $id the product ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_product( $id, $force = false ) { + + $id = $this->validate_request( $id, 'product', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + return $this->delete( $id, 'product', ( 'true' === $force ) ); + } + + /** + * Get the reviews for a product + * + * @since 2.1 + * @param int $id the product ID to get reviews for + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_product_reviews( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $args = array( + 'post_id' => $id, + 'approve' => 'approve', + ); + + $comments = get_comments( $args ); + + $reviews = array(); + + foreach ( $comments as $comment ) { + + $reviews[] = array( + 'id' => $comment->comment_ID, + 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), + 'review' => $comment->comment_content, + 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), + 'reviewer_name' => $comment->comment_author, + 'reviewer_email' => $comment->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), + ); + } + + return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); + } + + /** + * Helper method to get product post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_products( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'product', + 'post_status' => 'publish', + 'meta_query' => array(), + ); + + if ( ! empty( $args['type'] ) ) { + + $types = explode( ',', $args['type'] ); + + $query_args['tax_query'] = array( + array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $types, + ), + ); + + unset( $args['type'] ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Get standard product data that applies to every product type + * + * @since 2.1 + * @param WC_Product|int $product + * @return array + */ + private function get_product_data( $product ) { + if ( is_numeric( $product ) ) { + $product = wc_get_product( $product ); + } + + return array( + 'title' => $product->get_name(), + 'id' => $product->get_id(), + 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'downloadable' => $product->is_downloadable(), + 'virtual' => $product->is_virtual(), + 'permalink' => $product->get_permalink(), + 'sku' => $product->get_sku(), + 'price' => wc_format_decimal( $product->get_price(), 2 ), + 'regular_price' => wc_format_decimal( $product->get_regular_price(), 2 ), + 'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), 2 ) : null, + 'price_html' => $product->get_price_html(), + 'taxable' => $product->is_taxable(), + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'managing_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'purchaseable' => $product->is_purchasable(), + 'featured' => $product->is_featured(), + 'visible' => $product->is_visible(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'on_sale' => $product->is_on_sale(), + 'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null, + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, + 'description' => apply_filters( 'the_content', $product->get_description() ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), + 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), + 'images' => $this->get_images( $product ), + 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), + 'attributes' => $this->get_attributes( $product ), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'download_type' => 'standard', + 'purchase_note' => apply_filters( 'the_content', $product->get_purchase_note() ), + 'total_sales' => $product->get_total_sales(), + 'variations' => array(), + 'parent' => array(), + ); + } + + /** + * Get an individual variation's data + * + * @since 2.1 + * @param WC_Product $product + * @return array + */ + private function get_variation_data( $product ) { + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + $variation = wc_get_product( $child_id ); + + if ( ! $variation || ! $variation->exists() ) { + continue; + } + + $variations[] = array( + 'id' => $variation->get_id(), + 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), + 'downloadable' => $variation->is_downloadable(), + 'virtual' => $variation->is_virtual(), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => wc_format_decimal( $variation->get_price(), 2 ), + 'regular_price' => wc_format_decimal( $variation->get_regular_price(), 2 ), + 'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), 2 ) : null, + 'taxable' => $variation->is_taxable(), + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'stock_quantity' => (int) $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backordered' => $variation->is_on_backorder(), + 'purchaseable' => $variation->is_purchasable(), + 'visible' => $variation->variation_is_visible(), + 'on_sale' => $variation->is_on_sale(), + 'weight' => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null, + 'dimensions' => array( + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $product->get_download_limit(), + 'download_expiry' => (int) $product->get_download_expiry(), + ); + } + + return $variations; + } + + /** + * Get the images for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_images( $product ) { + $images = $attachment_ids = array(); + $product_image = $product->get_image_id(); + + // Add featured image. + if ( ! empty( $product_image ) ) { + $attachment_ids[] = $product_image; + } + + // add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + + $attachment_post = get_post( $attachment_id ); + + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'title' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + + $images[] = array( + 'id' => 0, + 'created_at' => $this->server->format_datetime( time() ), // default to now + 'updated_at' => $this->server->format_datetime( time() ), + 'src' => wc_placeholder_img_src(), + 'title' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get attribute options. + * + * @param int $product_id + * @param array $attribute + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_attributes( $product ) { + + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + + // variation attributes + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + + // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` + $attributes[] = array( + 'name' => ucwords( str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => ucwords( str_replace( 'pa_', '', $attribute['name'] ) ), + 'position' => $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get the downloads for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_downloads( $product ) { + + $downloads = array(); + + if ( $product->is_downloadable() ) { + + foreach ( $product->get_downloads() as $file_id => $file ) { + + $downloads[] = array( + 'id' => $file_id, // do not cast as int as this is a hash + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } +} diff --git a/includes/api/legacy/v1/class-wc-api-reports.php b/includes/api/legacy/v1/class-wc-api-reports.php new file mode 100644 index 00000000000..527ea64bf94 --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-reports.php @@ -0,0 +1,482 @@ +base ] = array( + array( array( $this, 'get_reports' ), WC_API_Server::READABLE ), + ); + + # GET /reports/sales + $routes[ $this->base . '/sales' ] = array( + array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ), + ); + + # GET /reports/sales/top_sellers + $routes[ $this->base . '/sales/top_sellers' ] = array( + array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ), + ); + + return $routes; + } + + /** + * Get a simple listing of available reports + * + * @since 2.1 + * @return array + */ + public function get_reports() { + + return array( 'reports' => array( 'sales', 'sales/top_sellers' ) ); + } + + /** + * Get the sales report + * + * @since 2.1 + * @param string $fields fields to include in response + * @param array $filter date filtering + * @return array|WP_Error + */ + public function get_sales_report( $fields = null, $filter = array() ) { + + // check user permissions + $check = $this->validate_request(); + + if ( is_wp_error( $check ) ) { + return $check; + } + + // set date filtering + $this->setup_report( $filter ); + + // total sales, taxes, shipping, and order count + $totals = $this->report->get_order_report_data( array( + 'data' => array( + '_order_total' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'sales', + ), + '_order_tax' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'tax', + ), + '_order_shipping_tax' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'shipping_tax', + ), + '_order_shipping' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'shipping', + ), + 'ID' => array( + 'type' => 'post_data', + 'function' => 'COUNT', + 'name' => 'order_count', + ), + ), + 'filter_range' => true, + ) ); + + // total items ordered + $total_items = absint( $this->report->get_order_report_data( array( + 'data' => array( + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ), + ), + 'query_type' => 'get_var', + 'filter_range' => true, + ) ) ); + + // total discount used + $total_discount = $this->report->get_order_report_data( array( + 'data' => array( + 'discount_amount' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'coupon', + 'function' => 'SUM', + 'name' => 'discount_amount', + ), + ), + 'where' => array( + array( + 'key' => 'order_item_type', + 'value' => 'coupon', + 'operator' => '=', + ), + ), + 'query_type' => 'get_var', + 'filter_range' => true, + ) ); + + // new customers + $users_query = new WP_User_Query( + array( + 'fields' => array( 'user_registered' ), + 'role' => 'customer', + ) + ); + + $customers = $users_query->get_results(); + + foreach ( $customers as $key => $customer ) { + if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { + unset( $customers[ $key ] ); + } + } + + $total_customers = count( $customers ); + + // get order totals grouped by period + $orders = $this->report->get_order_report_data( array( + 'data' => array( + '_order_total' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_sales', + ), + '_order_shipping' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_shipping', + ), + '_order_tax' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_tax', + ), + '_order_shipping_tax' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_shipping_tax', + ), + 'ID' => array( + 'type' => 'post_data', + 'function' => 'COUNT', + 'name' => 'total_orders', + 'distinct' => true, + ), + 'post_date' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'post_date', + ), + ), + 'group_by' => $this->report->group_by_query, + 'order_by' => 'post_date ASC', + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + // get order item totals grouped by period + $order_items = $this->report->get_order_report_data( array( + 'data' => array( + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_count', + ), + 'post_date' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'post_date', + ), + ), + 'where' => array( + array( + 'key' => 'order_item_type', + 'value' => 'line_item', + 'operator' => '=', + ), + ), + 'group_by' => $this->report->group_by_query, + 'order_by' => 'post_date ASC', + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + // get discount totals grouped by period + $discounts = $this->report->get_order_report_data( array( + 'data' => array( + 'discount_amount' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'coupon', + 'function' => 'SUM', + 'name' => 'discount_amount', + ), + 'post_date' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'post_date', + ), + ), + 'where' => array( + array( + 'key' => 'order_item_type', + 'value' => 'coupon', + 'operator' => '=', + ), + ), + 'group_by' => $this->report->group_by_query . ', order_item_name', + 'order_by' => 'post_date ASC', + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + $period_totals = array(); + + // setup period totals by ensuring each period in the interval has data + for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { + + switch ( $this->report->chart_groupby ) { + case 'day' : + $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); + break; + case 'month' : + $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); + break; + } + + // set the customer signups for each period + $customer_count = 0; + foreach ( $customers as $customer ) { + + if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { + $customer_count++; + } + } + + $period_totals[ $time ] = array( + 'sales' => wc_format_decimal( 0.00, 2 ), + 'orders' => 0, + 'items' => 0, + 'tax' => wc_format_decimal( 0.00, 2 ), + 'shipping' => wc_format_decimal( 0.00, 2 ), + 'discount' => wc_format_decimal( 0.00, 2 ), + 'customers' => $customer_count, + ); + } + + // add total sales, total order count, total tax and total shipping for each period + foreach ( $orders as $order ) { + + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); + $period_totals[ $time ]['orders'] = (int) $order->total_orders; + $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); + $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); + } + + // add total order items for each period + foreach ( $order_items as $order_item ) { + + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; + } + + // add total discount for each period + foreach ( $discounts as $discount ) { + + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); + } + + $sales_data = array( + 'total_sales' => wc_format_decimal( $totals->sales, 2 ), + 'average_sales' => wc_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ), + 'total_orders' => (int) $totals->order_count, + 'total_items' => $total_items, + 'total_tax' => wc_format_decimal( $totals->tax + $totals->shipping_tax, 2 ), + 'total_shipping' => wc_format_decimal( $totals->shipping, 2 ), + 'total_discount' => is_null( $total_discount ) ? wc_format_decimal( 0.00, 2 ) : wc_format_decimal( $total_discount, 2 ), + 'totals_grouped_by' => $this->report->chart_groupby, + 'totals' => $period_totals, + 'total_customers' => $total_customers, + ); + + return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) ); + } + + /** + * Get the top sellers report + * + * @since 2.1 + * @param string $fields fields to include in response + * @param array $filter date filtering + * @return array|WP_Error + */ + public function get_top_sellers_report( $fields = null, $filter = array() ) { + + // check user permissions + $check = $this->validate_request(); + + if ( is_wp_error( $check ) ) { + return $check; + } + + // set date filtering + $this->setup_report( $filter ); + + $top_sellers = $this->report->get_order_report_data( array( + 'data' => array( + '_product_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => '', + 'name' => 'product_id', + ), + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ), + ), + 'order_by' => 'order_item_qty DESC', + 'group_by' => 'product_id', + 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + $top_sellers_data = array(); + + foreach ( $top_sellers as $top_seller ) { + + $product = wc_get_product( $top_seller->product_id ); + + $top_sellers_data[] = array( + 'title' => $product->get_name(), + 'product_id' => $top_seller->product_id, + 'quantity' => $top_seller->order_item_qty, + ); + } + + return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) ); + } + + /** + * Setup the report object and parse any date filtering + * + * @since 2.1 + * @param array $filter date filtering + */ + private function setup_report( $filter ) { + + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); + + $this->report = new WC_Admin_Report(); + + if ( empty( $filter['period'] ) ) { + + // custom date range + $filter['period'] = 'custom'; + + if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { + + // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges + $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] ); + $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null; + + } else { + + // default custom range to today + $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); + } + } else { + + // ensure period is valid + if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { + $filter['period'] = 'week'; + } + + // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods + // allow "week" for period instead of "7day" + if ( 'week' === $filter['period'] ) { + $filter['period'] = '7day'; + } + } + + $this->report->calculate_current_range( $filter['period'] ); + } + + /** + * Verify that the current user has permission to view reports + * + * @since 2.1 + * @see WC_API_Resource::validate_request() + * @param null $id unused + * @param null $type unused + * @param null $context unused + * @return true|WP_Error + */ + protected function validate_request( $id = null, $type = null, $context = null ) { + + if ( ! current_user_can( 'view_woocommerce_reports' ) ) { + + return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) ); + + } else { + + return true; + } + } +} diff --git a/includes/api/legacy/v1/class-wc-api-resource.php b/includes/api/legacy/v1/class-wc-api-resource.php new file mode 100644 index 00000000000..c298e47a078 --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-resource.php @@ -0,0 +1,410 @@ +server = $server; + + // automatically register routes for sub-classes + add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); + + // remove fields from responses when requests specify certain fields + // note these are hooked at a later priority so data added via filters (e.g. customer data to the order response) + // still has the fields filtered properly + foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { + + add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); + add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 ); + } + } + + /** + * Validate the request by checking: + * + * 1) the ID is a valid integer + * 2) the ID returns a valid post object and matches the provided post type + * 3) the current user has the proper permissions to read/edit/delete the post + * + * @since 2.1 + * @param string|int $id the post ID + * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` + * @param string $context the context of the request, either `read`, `edit` or `delete` + * @return int|WP_Error valid post ID or WP_Error if any of the checks fails + */ + protected function validate_request( $id, $type, $context ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + $id = absint( $id ); + + // validate ID + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); + } + + // only custom post types have per-post type/permission checks + if ( 'customer' !== $type ) { + + $post = get_post( $id ); + + // for checking permissions, product variations are the same as the product post type + $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; + + // validate post type + if ( $type !== $post_type ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); + } + + // validate permissions + switch ( $context ) { + + case 'read': + if ( ! $this->is_readable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'edit': + if ( ! $this->is_editable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'delete': + if ( ! $this->is_deletable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + } + } + + return $id; + } + + /** + * Add common request arguments to argument list before WP_Query is run + * + * @since 2.1 + * @param array $base_args required arguments for the query (e.g. `post_type`, etc) + * @param array $request_args arguments provided in the request + * @return array + */ + protected function merge_query_args( $base_args, $request_args ) { + + $args = array(); + + // date + if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { + + $args['date_query'] = array(); + + // resources created after specified date + if ( ! empty( $request_args['created_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); + } + + // resources created before specified date + if ( ! empty( $request_args['created_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); + } + + // resources updated after specified date + if ( ! empty( $request_args['updated_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); + } + + // resources updated before specified date + if ( ! empty( $request_args['updated_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); + } + } + + // search + if ( ! empty( $request_args['q'] ) ) { + $args['s'] = $request_args['q']; + } + + // resources per response + if ( ! empty( $request_args['limit'] ) ) { + $args['posts_per_page'] = $request_args['limit']; + } + + // resource offset + if ( ! empty( $request_args['offset'] ) ) { + $args['offset'] = $request_args['offset']; + } + + // resource page + $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; + + return array_merge( $base_args, $args ); + } + + /** + * Add meta to resources when requested by the client. Meta is added as a top-level + * `_meta` attribute (e.g. `order_meta`) as a list of key/value pairs + * + * @since 2.1 + * @param array $data the resource data + * @param object $resource the resource object (e.g WC_Order) + * @return mixed + */ + public function maybe_add_meta( $data, $resource ) { + + if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { + + // don't attempt to add meta more than once + if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { + return $data; + } + + // define the top-level property name for the meta + switch ( get_class( $resource ) ) { + + case 'WC_Order': + $meta_name = 'order_meta'; + break; + + case 'WC_Coupon': + $meta_name = 'coupon_meta'; + break; + + case 'WP_User': + $meta_name = 'customer_meta'; + break; + + default: + $meta_name = 'product_meta'; + break; + } + + if ( is_a( $resource, 'WP_User' ) ) { + + // customer meta + $meta = (array) get_user_meta( $resource->ID ); + + } else { + + // coupon/order/product meta + $meta = (array) get_post_meta( $resource->get_id() ); + } + + foreach ( $meta as $meta_key => $meta_value ) { + + // don't add hidden meta by default + if ( ! is_protected_meta( $meta_key ) ) { + $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); + } + } + } + + return $data; + } + + /** + * Restrict the fields included in the response if the request specified certain only certain fields should be returned + * + * @since 2.1 + * @param array $data the response data + * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order + * @param array|string the requested list of fields to include in the response + * @return array response data + */ + public function filter_response_fields( $data, $resource, $fields ) { + + if ( ! is_array( $data ) || empty( $fields ) ) { + return $data; + } + + $fields = explode( ',', $fields ); + $sub_fields = array(); + + // get sub fields + foreach ( $fields as $field ) { + + if ( false !== strpos( $field, '.' ) ) { + + list( $name, $value ) = explode( '.', $field ); + + $sub_fields[ $name ] = $value; + } + } + + // iterate through top-level fields + foreach ( $data as $data_field => $data_value ) { + + // if a field has sub-fields and the top-level field has sub-fields to filter + if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { + + // iterate through each sub-field + foreach ( $data_value as $sub_field => $sub_field_value ) { + + // remove non-matching sub-fields + if ( ! in_array( $sub_field, $sub_fields ) ) { + unset( $data[ $data_field ][ $sub_field ] ); + } + } + } else { + + // remove non-matching top-level fields + if ( ! in_array( $data_field, $fields ) ) { + unset( $data[ $data_field ] ); + } + } + } + + return $data; + } + + /** + * Delete a given resource + * + * @since 2.1 + * @param int $id the resource ID + * @param string $type the resource post type, or `customer` + * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) + * @return array|WP_Error + */ + protected function delete( $id, $type, $force = false ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + if ( 'customer' === $type ) { + + $result = wp_delete_user( $id ); + + if ( $result ) { + return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); + } else { + return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); + } + } else { + + // delete order/coupon/product + $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); + + if ( ! $result ) { + return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); + + } else { + + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); + } + } + } + + + /** + * Checks if the given post is readable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_readable( $post ) { + + return $this->check_permission( $post, 'read' ); + } + + /** + * Checks if the given post is editable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_editable( $post ) { + + return $this->check_permission( $post, 'edit' ); + + } + + /** + * Checks if the given post is deletable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_deletable( $post ) { + + return $this->check_permission( $post, 'delete' ); + } + + /** + * Checks the permissions for the current user given a post and context + * + * @since 2.1 + * @param WP_Post|int $post + * @param string $context the type of permission to check, either `read`, `write`, or `delete` + * @return bool true if the current user has the permissions to perform the context on the post + */ + private function check_permission( $post, $context ) { + + if ( ! is_a( $post, 'WP_Post' ) ) { + $post = get_post( $post ); + } + + if ( is_null( $post ) ) { + return false; + } + + $post_type = get_post_type_object( $post->post_type ); + + if ( 'read' === $context ) { + return current_user_can( $post_type->cap->read_private_posts, $post->ID ); + } elseif ( 'edit' === $context ) { + return current_user_can( $post_type->cap->edit_post, $post->ID ); + } elseif ( 'delete' === $context ) { + return current_user_can( $post_type->cap->delete_post, $post->ID ); + } else { + return false; + } + } +} diff --git a/includes/api/legacy/v1/class-wc-api-server.php b/includes/api/legacy/v1/class-wc-api-server.php new file mode 100644 index 00000000000..6c85fcf4ade --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-server.php @@ -0,0 +1,781 @@ + self::METHOD_GET, + 'GET' => self::METHOD_GET, + 'POST' => self::METHOD_POST, + 'PUT' => self::METHOD_PUT, + 'PATCH' => self::METHOD_PATCH, + 'DELETE' => self::METHOD_DELETE, + ); + + /** + * Requested path (relative to the API root, wp-json.php) + * + * @var string + */ + public $path = ''; + + /** + * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE) + * + * @var string + */ + public $method = 'HEAD'; + + /** + * Request parameters + * + * This acts as an abstraction of the superglobals + * (GET => $_GET, POST => $_POST) + * + * @var array + */ + public $params = array( 'GET' => array(), 'POST' => array() ); + + /** + * Request headers + * + * @var array + */ + public $headers = array(); + + /** + * Request files (matches $_FILES) + * + * @var array + */ + public $files = array(); + + /** + * Request/Response handler, either JSON by default + * or XML if requested by client + * + * @var WC_API_Handler + */ + public $handler; + + + /** + * Setup class and set request/response handler + * + * @since 2.1 + * @param $path + * @return WC_API_Server + */ + public function __construct( $path ) { + + if ( empty( $path ) ) { + if ( isset( $_SERVER['PATH_INFO'] ) ) { + $path = $_SERVER['PATH_INFO']; + } else { + $path = '/'; + } + } + + $this->path = $path; + $this->method = $_SERVER['REQUEST_METHOD']; + $this->params['GET'] = $_GET; + $this->params['POST'] = $_POST; + $this->headers = $this->get_headers( $_SERVER ); + $this->files = $_FILES; + + // Compatibility for clients that can't use PUT/PATCH/DELETE + if ( isset( $_GET['_method'] ) ) { + $this->method = strtoupper( $_GET['_method'] ); + } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { + $this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; + } + + // determine type of request/response and load handler, JSON by default + if ( $this->is_json_request() ) { + $handler_class = 'WC_API_JSON_Handler'; + } elseif ( $this->is_xml_request() ) { + $handler_class = 'WC_API_XML_Handler'; + } else { + $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this ); + } + + $this->handler = new $handler_class(); + } + + /** + * Check authentication for the request + * + * @since 2.1 + * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login + */ + public function check_authentication() { + + // allow plugins to remove default authentication or add their own authentication + $user = apply_filters( 'woocommerce_api_check_authentication', null, $this ); + + // API requests run under the context of the authenticated user + if ( is_a( $user, 'WP_User' ) ) { + wp_set_current_user( $user->ID ); + } elseif ( ! is_wp_error( $user ) ) { + // WP_Errors are handled in serve_request() + $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) ); + } + + return $user; + } + + /** + * Convert an error to an array + * + * This iterates over all error codes and messages to change it into a flat + * array. This enables simpler client behaviour, as it is represented as a + * list in JSON rather than an object/map + * + * @since 2.1 + * @param WP_Error $error + * @return array List of associative arrays with code and message keys + */ + protected function error_to_array( $error ) { + $errors = array(); + foreach ( (array) $error->errors as $code => $messages ) { + foreach ( (array) $messages as $message ) { + $errors[] = array( 'code' => $code, 'message' => $message ); + } + } + return array( 'errors' => $errors ); + } + + /** + * Handle serving an API request + * + * Matches the current server URI to a route and runs the first matching + * callback then outputs a JSON representation of the returned value. + * + * @since 2.1 + * @uses WC_API_Server::dispatch() + */ + public function serve_request() { + + do_action( 'woocommerce_api_server_before_serve', $this ); + + $this->header( 'Content-Type', $this->handler->get_content_type(), true ); + + // the API is enabled by default + if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) { + + $this->send_status( 404 ); + + echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) ); + + return; + } + + $result = $this->check_authentication(); + + // if authorization check was successful, dispatch the request + if ( ! is_wp_error( $result ) ) { + $result = $this->dispatch(); + } + + // handle any dispatch errors + if ( is_wp_error( $result ) ) { + $data = $result->get_error_data(); + if ( is_array( $data ) && isset( $data['status'] ) ) { + $this->send_status( $data['status'] ); + } + + $result = $this->error_to_array( $result ); + } + + // This is a filter rather than an action, since this is designed to be + // re-entrant if needed + $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this ); + + if ( ! $served ) { + + if ( 'HEAD' === $this->method ) { + return; + } + + echo $this->handler->generate_response( $result ); + } + } + + /** + * Retrieve the route map + * + * The route map is an associative array with path regexes as the keys. The + * value is an indexed array with the callback function/method as the first + * item, and a bitmask of HTTP methods as the second item (see the class + * constants). + * + * Each route can be mapped to more than one callback by using an array of + * the indexed arrays. This allows mapping e.g. GET requests to one callback + * and POST requests to another. + * + * Note that the path regexes (array keys) must have @ escaped, as this is + * used as the delimiter with preg_match() + * + * @since 2.1 + * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` + */ + public function get_routes() { + + // index added by default + $endpoints = array( + + '/' => array( array( $this, 'get_index' ), self::READABLE ), + ); + + $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints ); + + // Normalise the endpoints + foreach ( $endpoints as $route => &$handlers ) { + if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) { + $handlers = array( $handlers ); + } + } + + return $endpoints; + } + + /** + * Match the request to a callback and call it + * + * @since 2.1 + * @return mixed The value returned by the callback, or a WP_Error instance + */ + public function dispatch() { + + switch ( $this->method ) { + + case 'HEAD': + case 'GET': + $method = self::METHOD_GET; + break; + + case 'POST': + $method = self::METHOD_POST; + break; + + case 'PUT': + $method = self::METHOD_PUT; + break; + + case 'PATCH': + $method = self::METHOD_PATCH; + break; + + case 'DELETE': + $method = self::METHOD_DELETE; + break; + + default: + return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) ); + } + + foreach ( $this->get_routes() as $route => $handlers ) { + foreach ( $handlers as $handler ) { + $callback = $handler[0]; + $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; + + if ( ! ( $supported & $method ) ) { + continue; + } + + $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args ); + + if ( ! $match ) { + continue; + } + + if ( ! is_callable( $callback ) ) { + return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $args = array_merge( $args, $this->params['GET'] ); + if ( $method & self::METHOD_POST ) { + $args = array_merge( $args, $this->params['POST'] ); + } + if ( $supported & self::ACCEPT_DATA ) { + $data = $this->handler->parse_body( $this->get_raw_data() ); + $args = array_merge( $args, array( 'data' => $data ) ); + } elseif ( $supported & self::ACCEPT_RAW_DATA ) { + $data = $this->get_raw_data(); + $args = array_merge( $args, array( 'data' => $data ) ); + } + + $args['_method'] = $method; + $args['_route'] = $route; + $args['_path'] = $this->path; + $args['_headers'] = $this->headers; + $args['_files'] = $this->files; + + $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback ); + + // Allow plugins to halt the request via this filter + if ( is_wp_error( $args ) ) { + return $args; + } + + $params = $this->sort_callback_params( $callback, $args ); + if ( is_wp_error( $params ) ) { + return $params; + } + + return call_user_func_array( $callback, $params ); + } + } + + return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) ); + } + + /** + * Sort parameters by order specified in method declaration + * + * Takes a callback and a list of available params, then filters and sorts + * by the parameters the method actually needs, using the Reflection API + * + * @since 2.1 + * + * @param callable|array $callback the endpoint callback + * @param array $provided the provided request parameters + * + * @return array|WP_Error + */ + protected function sort_callback_params( $callback, $provided ) { + if ( is_array( $callback ) ) { + $ref_func = new ReflectionMethod( $callback[0], $callback[1] ); + } else { + $ref_func = new ReflectionFunction( $callback ); + } + + $wanted = $ref_func->getParameters(); + $ordered_parameters = array(); + + foreach ( $wanted as $param ) { + if ( isset( $provided[ $param->getName() ] ) ) { + // We have this parameters in the list to choose from + $ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] ); + } elseif ( $param->isDefaultValueAvailable() ) { + // We don't have this parameter, but it's optional + $ordered_parameters[] = $param->getDefaultValue(); + } else { + // We don't have this parameter and it wasn't optional, abort! + return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) ); + } + } + return $ordered_parameters; + } + + /** + * Get the site index. + * + * This endpoint describes the capabilities of the site. + * + * @since 2.1 + * @return array Index entity + */ + public function get_index() { + + // General site data + $available = array( + 'store' => array( + 'name' => get_option( 'blogname' ), + 'description' => get_option( 'blogdescription' ), + 'URL' => get_option( 'siteurl' ), + 'wc_version' => WC()->version, + 'routes' => array(), + 'meta' => array( + 'timezone' => wc_timezone_string(), + 'currency' => get_woocommerce_currency(), + 'currency_format' => get_woocommerce_currency_symbol(), + 'tax_included' => wc_prices_include_tax(), + 'weight_unit' => get_option( 'woocommerce_weight_unit' ), + 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), + 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ), + 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ), + 'links' => array( + 'help' => 'https://woocommerce.github.io/woocommerce/rest-api/', + ), + ), + ), + ); + + // Find the available routes + foreach ( $this->get_routes() as $route => $callbacks ) { + $data = array(); + + $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); + $methods = array(); + foreach ( self::$method_map as $name => $bitmask ) { + foreach ( $callbacks as $callback ) { + // Skip to the next route if any callback is hidden + if ( $callback[1] & self::HIDDEN_ENDPOINT ) { + continue 3; + } + + if ( $callback[1] & $bitmask ) { + $data['supports'][] = $name; + } + + if ( $callback[1] & self::ACCEPT_DATA ) { + $data['accepts_data'] = true; + } + + // For non-variable routes, generate links + if ( strpos( $route, '<' ) === false ) { + $data['meta'] = array( + 'self' => get_woocommerce_api_url( $route ), + ); + } + } + } + $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data ); + } + return apply_filters( 'woocommerce_api_index', $available ); + } + + /** + * Send a HTTP status code + * + * @since 2.1 + * @param int $code HTTP status + */ + public function send_status( $code ) { + status_header( $code ); + } + + /** + * Send a HTTP header + * + * @since 2.1 + * @param string $key Header key + * @param string $value Header value + * @param boolean $replace Should we replace the existing header? + */ + public function header( $key, $value, $replace = true ) { + header( sprintf( '%s: %s', $key, $value ), $replace ); + } + + /** + * Send a Link header + * + * @internal The $rel parameter is first, as this looks nicer when sending multiple + * + * @link http://tools.ietf.org/html/rfc5988 + * @link http://www.iana.org/assignments/link-relations/link-relations.xml + * + * @since 2.1 + * @param string $rel Link relation. Either a registered type, or an absolute URL + * @param string $link Target IRI for the link + * @param array $other Other parameters to send, as an associative array + */ + public function link_header( $rel, $link, $other = array() ) { + + $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) ); + + foreach ( $other as $key => $value ) { + + if ( 'title' == $key ) { + + $value = '"' . $value . '"'; + } + + $header .= '; ' . $key . '=' . $value; + } + + $this->header( 'Link', $header, false ); + } + + /** + * Send pagination headers for resources + * + * @since 2.1 + * @param WP_Query|WP_User_Query $query + */ + public function add_pagination_headers( $query ) { + + // WP_User_Query + if ( is_a( $query, 'WP_User_Query' ) ) { + + $page = $query->page; + $single = count( $query->get_results() ) == 1; + $total = $query->get_total(); + $total_pages = $query->total_pages; + + // WP_Query + } else { + + $page = $query->get( 'paged' ); + $single = $query->is_single(); + $total = $query->found_posts; + $total_pages = $query->max_num_pages; + } + + if ( ! $page ) { + $page = 1; + } + + $next_page = absint( $page ) + 1; + + if ( ! $single ) { + + // first/prev + if ( $page > 1 ) { + $this->link_header( 'first', $this->get_paginated_url( 1 ) ); + $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) ); + } + + // next + if ( $next_page <= $total_pages ) { + $this->link_header( 'next', $this->get_paginated_url( $next_page ) ); + } + + // last + if ( $page != $total_pages ) { + $this->link_header( 'last', $this->get_paginated_url( $total_pages ) ); + } + } + + $this->header( 'X-WC-Total', $total ); + $this->header( 'X-WC-TotalPages', $total_pages ); + + do_action( 'woocommerce_api_pagination_headers', $this, $query ); + } + + /** + * Returns the request URL with the page query parameter set to the specified page + * + * @since 2.1 + * @param int $page + * @return string + */ + private function get_paginated_url( $page ) { + + // remove existing page query param + $request = remove_query_arg( 'page' ); + + // add provided page query param + $request = urldecode( add_query_arg( 'page', $page, $request ) ); + + // get the home host + $host = parse_url( get_home_url(), PHP_URL_HOST ); + + return set_url_scheme( "http://{$host}{$request}" ); + } + + /** + * Retrieve the raw request entity (body) + * + * @since 2.1 + * @return string + */ + public function get_raw_data() { + // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6 + if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { + return file_get_contents( 'php://input' ); + } + + global $HTTP_RAW_POST_DATA; + + // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, + // but we can do it ourself. + if ( ! isset( $HTTP_RAW_POST_DATA ) ) { + $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); + } + + return $HTTP_RAW_POST_DATA; + } + + /** + * Parse an RFC3339 datetime into a MySQl datetime + * + * Invalid dates default to unix epoch + * + * @since 2.1 + * @param string $datetime RFC3339 datetime + * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) + */ + public function parse_datetime( $datetime ) { + + // Strip millisecond precision (a full stop followed by one or more digits) + if ( strpos( $datetime, '.' ) !== false ) { + $datetime = preg_replace( '/\.\d+/', '', $datetime ); + } + + // default timezone to UTC + $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); + + try { + + $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); + + } catch ( Exception $e ) { + + $datetime = new DateTime( '@0' ); + + } + + return $datetime->format( 'Y-m-d H:i:s' ); + } + + /** + * Format a unix timestamp or MySQL datetime into an RFC3339 datetime + * + * @since 2.1 + * @param int|string $timestamp unix timestamp or MySQL datetime + * @param bool $convert_to_utc + * @param bool $convert_to_gmt Use GMT timezone. + * @return string RFC3339 datetime + */ + public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) { + if ( $convert_to_gmt ) { + if ( is_numeric( $timestamp ) ) { + $timestamp = date( 'Y-m-d H:i:s', $timestamp ); + } + + $timestamp = get_gmt_from_date( $timestamp ); + } + + if ( $convert_to_utc ) { + $timezone = new DateTimeZone( wc_timezone_string() ); + } else { + $timezone = new DateTimeZone( 'UTC' ); + } + + try { + + if ( is_numeric( $timestamp ) ) { + $date = new DateTime( "@{$timestamp}" ); + } else { + $date = new DateTime( $timestamp, $timezone ); + } + + // convert to UTC by adjusting the time based on the offset of the site's timezone + if ( $convert_to_utc ) { + $date->modify( -1 * $date->getOffset() . ' seconds' ); + } + } catch ( Exception $e ) { + + $date = new DateTime( '@0' ); + } + + return $date->format( 'Y-m-d\TH:i:s\Z' ); + } + + /** + * Extract headers from a PHP-style $_SERVER array + * + * @since 2.1 + * @param array $server Associative array similar to $_SERVER + * @return array Headers extracted from the input + */ + public function get_headers( $server ) { + $headers = array(); + // CONTENT_* headers are not prefixed with HTTP_ + $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true ); + + foreach ( $server as $key => $value ) { + if ( strpos( $key, 'HTTP_' ) === 0 ) { + $headers[ substr( $key, 5 ) ] = $value; + } elseif ( isset( $additional[ $key ] ) ) { + $headers[ $key ] = $value; + } + } + + return $headers; + } + + /** + * Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or + * the HTTP ACCEPT header + * + * @since 2.1 + * @return bool + */ + private function is_json_request() { + + // check path + if ( false !== stripos( $this->path, '.json' ) ) { + return true; + } + + // check ACCEPT header, only 'application/json' is acceptable, see RFC 4627 + if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] ) { + return true; + } + + return false; + } + + /** + * Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or + * the HTTP ACCEPT header + * + * @since 2.1 + * @return bool + */ + private function is_xml_request() { + + // check path + if ( false !== stripos( $this->path, '.xml' ) ) { + return true; + } + + // check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376 + if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) ) { + return true; + } + + return false; + } +} diff --git a/includes/api/legacy/v1/class-wc-api-xml-handler.php b/includes/api/legacy/v1/class-wc-api-xml-handler.php new file mode 100644 index 00000000000..04f47e669e4 --- /dev/null +++ b/includes/api/legacy/v1/class-wc-api-xml-handler.php @@ -0,0 +1,308 @@ +xml = new XMLWriter(); + + $this->xml->openMemory(); + + $this->xml->setIndent( true ); + + $this->xml->startDocument( '1.0', 'UTF-8' ); + + $root_element = key( $data ); + + $data = $data[ $root_element ]; + + switch ( $root_element ) { + + case 'orders': + $data = array( 'order' => $data ); + break; + + case 'order_notes': + $data = array( 'order_note' => $data ); + break; + + case 'customers': + $data = array( 'customer' => $data ); + break; + + case 'coupons': + $data = array( 'coupon' => $data ); + break; + + case 'products': + $data = array( 'product' => $data ); + break; + + case 'product_reviews': + $data = array( 'product_review' => $data ); + break; + + default: + $data = apply_filters( 'woocommerce_api_xml_data', $data, $root_element ); + break; + } + + // generate xml starting with the root element and recursively generating child elements + $this->array_to_xml( $root_element, $data ); + + $this->xml->endDocument(); + + return $this->xml->outputMemory(); + } + + /** + * Convert array into XML by recursively generating child elements + * + * @since 2.1 + * @param string|array $element_key - name for element, e.g. + * @param string|array $element_value - value for element, e.g. 1234 + * @return string - generated XML + */ + private function array_to_xml( $element_key, $element_value = array() ) { + + if ( is_array( $element_value ) ) { + + // handle attributes + if ( '@attributes' === $element_key ) { + foreach ( $element_value as $attribute_key => $attribute_value ) { + + $this->xml->startAttribute( $attribute_key ); + $this->xml->text( $attribute_value ); + $this->xml->endAttribute(); + } + return; + } + + // handle multi-elements (e.g. multiple elements) + if ( is_numeric( key( $element_value ) ) ) { + + // recursively generate child elements + foreach ( $element_value as $child_element_key => $child_element_value ) { + + $this->xml->startElement( $element_key ); + + foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) { + $this->array_to_xml( $sibling_element_key, $sibling_element_value ); + } + + $this->xml->endElement(); + } + } else { + + // start root element + $this->xml->startElement( $element_key ); + + // recursively generate child elements + foreach ( $element_value as $child_element_key => $child_element_value ) { + $this->array_to_xml( $child_element_key, $child_element_value ); + } + + // end root element + $this->xml->endElement(); + } + } else { + + // handle single elements + if ( '@value' == $element_key ) { + + $this->xml->text( $element_value ); + + } else { + + // wrap element in CDATA tags if it contains illegal characters + if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) { + + $this->xml->startElement( $element_key ); + $this->xml->writeCdata( $element_value ); + $this->xml->endElement(); + + } else { + + $this->xml->writeElement( $element_key, $element_value ); + } + } + + return; + } + } + + /** + * Adjust the sales report array format to change totals keyed with the sales date to become an + * attribute for the totals element instead + * + * @since 2.1 + * @param array $data + * @return array + */ + public function format_sales_report_data( $data ) { + + if ( ! empty( $data['totals'] ) ) { + + foreach ( $data['totals'] as $date => $totals ) { + + unset( $data['totals'][ $date ] ); + + $data['totals'][] = array_merge( array( '@attributes' => array( 'date' => $date ) ), $totals ); + } + } + + return $data; + } + + /** + * Adjust the product data to handle options for attributes without a named child element and other + * fields that have no named child elements (e.g. categories = array( 'cat1', 'cat2' ) ) + * + * Note that the parent product data for variations is also adjusted in the same manner as needed + * + * @since 2.1 + * @param array $data + * @return array + */ + public function format_product_data( $data ) { + + // handle attribute values + if ( ! empty( $data['attributes'] ) ) { + + foreach ( $data['attributes'] as $attribute_key => $attribute ) { + + if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) { + + foreach ( $attribute['options'] as $option_key => $option ) { + + unset( $data['attributes'][ $attribute_key ]['options'][ $option_key ] ); + + $data['attributes'][ $attribute_key ]['options']['option'][] = array( $option ); + } + } + } + } + + // simple arrays are fine for JSON, but XML requires a child element name, so this adjusts the data + // array to define a child element name for each field + $fields_to_fix = array( + 'related_ids' => 'related_id', + 'upsell_ids' => 'upsell_id', + 'cross_sell_ids' => 'cross_sell_id', + 'categories' => 'category', + 'tags' => 'tag', + ); + + foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) { + + if ( ! empty( $data[ $parent_field_name ] ) ) { + + foreach ( $data[ $parent_field_name ] as $field_key => $field ) { + + unset( $data[ $parent_field_name ][ $field_key ] ); + + $data[ $parent_field_name ][ $child_field_name ][] = array( $field ); + } + } + } + + // handle adjusting the parent product for variations + if ( ! empty( $data['parent'] ) ) { + + // attributes + if ( ! empty( $data['parent']['attributes'] ) ) { + + foreach ( $data['parent']['attributes'] as $attribute_key => $attribute ) { + + if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) { + + foreach ( $attribute['options'] as $option_key => $option ) { + + unset( $data['parent']['attributes'][ $attribute_key ]['options'][ $option_key ] ); + + $data['parent']['attributes'][ $attribute_key ]['options']['option'][] = array( $option ); + } + } + } + } + + // fields + foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) { + + if ( ! empty( $data['parent'][ $parent_field_name ] ) ) { + + foreach ( $data['parent'][ $parent_field_name ] as $field_key => $field ) { + + unset( $data['parent'][ $parent_field_name ][ $field_key ] ); + + $data['parent'][ $parent_field_name ][ $child_field_name ][] = array( $field ); + } + } + } + } + + return $data; + } +} diff --git a/includes/api/legacy/v1/interface-wc-api-handler.php b/includes/api/legacy/v1/interface-wc-api-handler.php new file mode 100644 index 00000000000..464d9cb73cb --- /dev/null +++ b/includes/api/legacy/v1/interface-wc-api-handler.php @@ -0,0 +1,48 @@ +api->server->path ) { + return new WP_User( 0 ); + } + + try { + + if ( is_ssl() ) { + $keys = $this->perform_ssl_authentication(); + } else { + $keys = $this->perform_oauth_authentication(); + } + + // Check API key-specific permission + $this->check_api_key_permissions( $keys['permissions'] ); + + $user = $this->get_user_by_id( $keys['user_id'] ); + + $this->update_api_key_last_access( $keys['key_id'] ); + + } catch ( Exception $e ) { + $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + return $user; + } + + /** + * SSL-encrypted requests are not subject to sniffing or man-in-the-middle + * attacks, so the request can be authenticated by simply looking up the user + * associated with the given consumer key and confirming the consumer secret + * provided is valid + * + * @since 2.1 + * @return array + * @throws Exception + */ + private function perform_ssl_authentication() { + + $params = WC()->api->server->params['GET']; + + // Get consumer key + if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) { + + // Should be in HTTP Auth header by default + $consumer_key = $_SERVER['PHP_AUTH_USER']; + + } elseif ( ! empty( $params['consumer_key'] ) ) { + + // Allow a query string parameter as a fallback + $consumer_key = $params['consumer_key']; + + } else { + + throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 ); + } + + // Get consumer secret + if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { + + // Should be in HTTP Auth header by default + $consumer_secret = $_SERVER['PHP_AUTH_PW']; + + } elseif ( ! empty( $params['consumer_secret'] ) ) { + + // Allow a query string parameter as a fallback + $consumer_secret = $params['consumer_secret']; + + } else { + + throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 ); + } + + $keys = $this->get_keys_by_consumer_key( $consumer_key ); + + if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) { + throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 ); + } + + return $keys; + } + + /** + * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests + * + * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP + * + * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: + * + * 1) There is no token associated with request/responses, only consumer keys/secrets are used + * + * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, + * This is because there is no cross-OS function within PHP to get the raw Authorization header + * + * @link http://tools.ietf.org/html/rfc5849 for the full spec + * @since 2.1 + * @return array + * @throws Exception + */ + private function perform_oauth_authentication() { + + $params = WC()->api->server->params['GET']; + + $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); + + // Check for required OAuth parameters + foreach ( $param_names as $param_name ) { + + if ( empty( $params[ $param_name ] ) ) { + throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 ); + } + } + + // Fetch WP user by consumer key + $keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] ); + + // Perform OAuth validation + $this->check_oauth_signature( $keys, $params ); + $this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] ); + + // Authentication successful, return user + return $keys; + } + + /** + * Return the keys for the given consumer key + * + * @since 2.4.0 + * @param string $consumer_key + * @return array + * @throws Exception + */ + private function get_keys_by_consumer_key( $consumer_key ) { + global $wpdb; + + $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); + + $keys = $wpdb->get_row( $wpdb->prepare( " + SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces + FROM {$wpdb->prefix}woocommerce_api_keys + WHERE consumer_key = '%s' + ", $consumer_key ), ARRAY_A ); + + if ( empty( $keys ) ) { + throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 ); + } + + return $keys; + } + + /** + * Get user by ID + * + * @since 2.4.0 + * @param int $user_id + * @return WP_User + * @throws Exception + */ + private function get_user_by_id( $user_id ) { + $user = get_user_by( 'id', $user_id ); + + if ( ! $user ) { + throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 ); + } + + return $user; + } + + /** + * Check if the consumer secret provided for the given user is valid + * + * @since 2.1 + * @param string $keys_consumer_secret + * @param string $consumer_secret + * @return bool + */ + private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) { + return hash_equals( $keys_consumer_secret, $consumer_secret ); + } + + /** + * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer + * has a valid key/secret + * + * @param array $keys + * @param array $params the request parameters + * @throws Exception + */ + private function check_oauth_signature( $keys, $params ) { + + $http_method = strtoupper( WC()->api->server->method ); + + $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path ); + + // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature + $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); + unset( $params['oauth_signature'] ); + + // Remove filters and convert them from array to strings to void normalize issues + if ( isset( $params['filter'] ) ) { + $filters = $params['filter']; + unset( $params['filter'] ); + foreach ( $filters as $filter => $filter_value ) { + $params[ 'filter[' . $filter . ']' ] = $filter_value; + } + } + + // Normalize parameter key/values + $params = $this->normalize_parameters( $params ); + + // Sort parameters + if ( ! uksort( $params, 'strcmp' ) ) { + throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 ); + } + + // Form query string + $query_params = array(); + foreach ( $params as $param_key => $param_value ) { + + $query_params[] = $param_key . '%3D' . $param_value; // join with equals sign + } + $query_string = implode( '%26', $query_params ); // join with ampersand + + $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; + + if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { + throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 ); + } + + $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); + + $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) ); + + if ( ! hash_equals( $signature, $consumer_signature ) ) { + throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 ); + } + } + + /** + * Normalize each parameter by assuming each parameter may have already been + * encoded, so attempt to decode, and then re-encode according to RFC 3986 + * + * Note both the key and value is normalized so a filter param like: + * + * 'filter[period]' => 'week' + * + * is encoded to: + * + * 'filter%5Bperiod%5D' => 'week' + * + * This conforms to the OAuth 1.0a spec which indicates the entire query string + * should be URL encoded + * + * @since 2.1 + * @see rawurlencode() + * @param array $parameters un-normalized pararmeters + * @return array normalized parameters + */ + private function normalize_parameters( $parameters ) { + + $normalized_parameters = array(); + + foreach ( $parameters as $key => $value ) { + + // Percent symbols (%) must be double-encoded + $key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) ); + $value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); + + $normalized_parameters[ $key ] = $value; + } + + return $normalized_parameters; + } + + /** + * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where + * an attacker could attempt to re-send an intercepted request at a later time. + * + * - A timestamp is valid if it is within 15 minutes of now + * - A nonce is valid if it has not been used within the last 15 minutes + * + * @param array $keys + * @param int $timestamp the unix timestamp for when the request was made + * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated + * @throws Exception + */ + private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) { + global $wpdb; + + $valid_window = 15 * 60; // 15 minute window + + if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { + throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 ); + } + + $used_nonces = maybe_unserialize( $keys['nonces'] ); + + if ( empty( $used_nonces ) ) { + $used_nonces = array(); + } + + if ( in_array( $nonce, $used_nonces ) ) { + throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 ); + } + + $used_nonces[ $timestamp ] = $nonce; + + // Remove expired nonces + foreach ( $used_nonces as $nonce_timestamp => $nonce ) { + if ( $nonce_timestamp < ( time() - $valid_window ) ) { + unset( $used_nonces[ $nonce_timestamp ] ); + } + } + + $used_nonces = maybe_serialize( $used_nonces ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'nonces' => $used_nonces ), + array( 'key_id' => $keys['key_id'] ), + array( '%s' ), + array( '%d' ) + ); + } + + /** + * Check that the API keys provided have the proper key-specific permissions to either read or write API resources + * + * @param string $key_permissions + * @throws Exception if the permission check fails + */ + public function check_api_key_permissions( $key_permissions ) { + switch ( WC()->api->server->method ) { + + case 'HEAD': + case 'GET': + if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) { + throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 ); + } + break; + + case 'POST': + case 'PUT': + case 'PATCH': + case 'DELETE': + if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) { + throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 ); + } + break; + } + } + + /** + * Updated API Key last access datetime + * + * @since 2.4.0 + * + * @param int $key_id + */ + private function update_api_key_last_access( $key_id ) { + global $wpdb; + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'last_access' => current_time( 'mysql' ) ), + array( 'key_id' => $key_id ), + array( '%s' ), + array( '%d' ) + ); + } +} diff --git a/includes/api/legacy/v2/class-wc-api-coupons.php b/includes/api/legacy/v2/class-wc-api-coupons.php new file mode 100644 index 00000000000..f9f7dd82c28 --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-coupons.php @@ -0,0 +1,573 @@ + + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /coupons + $routes[ $this->base ] = array( + array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ), + array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /coupons/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /coupons/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_coupon' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), + array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ), + ); + + # GET /coupons/code/, note that coupon codes can contain spaces, dashes and underscores + $routes[ $this->base . '/code/(?P\w[\w\s\-]*)' ] = array( + array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ), + ); + + # POST|PUT /coupons/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all coupons + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param int $page + * @return array + */ + public function get_coupons( $fields = null, $filter = array(), $page = 1 ) { + + $filter['page'] = $page; + + $query = $this->query_coupons( $filter ); + + $coupons = array(); + + foreach ( $query->posts as $coupon_id ) { + + if ( ! $this->is_readable( $coupon_id ) ) { + continue; + } + + $coupons[] = current( $this->get_coupon( $coupon_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'coupons' => $coupons ); + } + + /** + * Get the coupon for the given ID + * + * @since 2.1 + * @param int $id the coupon ID + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_coupon( $id, $fields = null ) { + try { + + $id = $this->validate_request( $id, 'shop_coupon', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $coupon = new WC_Coupon( $id ); + + if ( 0 === $coupon->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 ); + } + + $coupon_data = array( + 'id' => $coupon->get_id(), + 'code' => $coupon->get_code(), + 'type' => $coupon->get_discount_type(), + 'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times. + 'amount' => wc_format_decimal( $coupon->get_amount(), 2 ), + 'individual_use' => $coupon->get_individual_use(), + 'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ), + 'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ), + 'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null, + 'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null, + 'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(), + 'usage_count' => (int) $coupon->get_usage_count(), + 'expiry_date' => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times. + 'enable_free_shipping' => $coupon->get_free_shipping(), + 'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ), + 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ), + 'exclude_sale_items' => $coupon->get_exclude_sale_items(), + 'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ), + 'maximum_amount' => wc_format_decimal( $coupon->get_maximum_amount(), 2 ), + 'customer_emails' => $coupon->get_email_restrictions(), + 'description' => $coupon->get_description(), + ); + + return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of coupons + * + * @since 2.1 + * + * @param array $filter + * + * @return array|WP_Error + */ + public function get_coupons_count( $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_shop_coupons' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 ); + } + + $query = $this->query_coupons( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the coupon for the given code + * + * @since 2.1 + * @param string $code the coupon code + * @param string $fields fields to include in response + * @return int|WP_Error + */ + public function get_coupon_by_code( $code, $fields = null ) { + global $wpdb; + + try { + $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) ); + + if ( is_null( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 ); + } + + return $this->get_coupon( $id, $fields ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a coupon + * + * @since 2.2 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_coupon( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['coupon'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 ); + } + + $data = $data['coupon']; + + // Check user permission + if ( ! current_user_can( 'publish_shop_coupons' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this ); + + // Check if coupon code is specified + if ( ! isset( $data['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 ); + } + + $coupon_code = wc_format_coupon_code( $data['code'] ); + $id_from_code = wc_get_coupon_id_by_code( $coupon_code ); + + if ( $id_from_code ) { + throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 ); + } + + $defaults = array( + 'type' => 'fixed_cart', + 'amount' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'exclude_product_ids' => array(), + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => '', + 'usage_count' => '', + 'expiry_date' => '', + 'enable_free_shipping' => false, + 'product_category_ids' => array(), + 'exclude_product_category_ids' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'customer_emails' => array(), + 'description' => '', + ); + + $coupon_data = wp_parse_args( $data, $defaults ); + + // Validate coupon types + if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 ); + } + + $new_coupon = array( + 'post_title' => $coupon_code, + 'post_content' => '', + 'post_status' => 'publish', + 'post_author' => get_current_user_id(), + 'post_type' => 'shop_coupon', + 'post_excerpt' => $coupon_data['description'], + ); + + $id = wp_insert_post( $new_coupon, true ); + + if ( is_wp_error( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 ); + } + + // Set coupon meta + update_post_meta( $id, 'discount_type', $coupon_data['type'] ); + update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) ); + update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' ); + update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) ); + update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) ); + update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) ); + update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) ); + update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) ); + update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) ); + update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) ); + update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) ); + update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' ); + update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) ); + update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) ); + update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' ); + update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) ); + update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) ); + update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) ); + + do_action( 'woocommerce_api_create_coupon', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_coupon( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a coupon + * + * @since 2.2 + * + * @param int $id the coupon ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_coupon( $id, $data ) { + + try { + if ( ! isset( $data['coupon'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 ); + } + + $data = $data['coupon']; + + $id = $this->validate_request( $id, 'shop_coupon', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this ); + + if ( isset( $data['code'] ) ) { + global $wpdb; + + $coupon_code = wc_format_coupon_code( $data['code'] ); + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 ); + } + + $updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) ); + + if ( 0 === $updated ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 ); + } + } + + if ( isset( $data['description'] ) ) { + $updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) ); + + if ( 0 === $updated ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 ); + } + } + + if ( isset( $data['type'] ) ) { + // Validate coupon types + if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 ); + } + update_post_meta( $id, 'discount_type', $data['type'] ); + } + + if ( isset( $data['amount'] ) ) { + update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); + } + + if ( isset( $data['individual_use'] ) ) { + update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); + } + + if ( isset( $data['product_ids'] ) ) { + update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); + } + + if ( isset( $data['exclude_product_ids'] ) ) { + update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); + } + + if ( isset( $data['usage_limit'] ) ) { + update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) ); + } + + if ( isset( $data['usage_limit_per_user'] ) ) { + update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); + } + + if ( isset( $data['limit_usage_to_x_items'] ) ) { + update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); + } + + if ( isset( $data['usage_count'] ) ) { + update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) ); + } + + if ( isset( $data['expiry_date'] ) ) { + update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); + update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) ); + } + + if ( isset( $data['enable_free_shipping'] ) ) { + update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' ); + } + + if ( isset( $data['product_category_ids'] ) ) { + update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) ); + } + + if ( isset( $data['exclude_product_category_ids'] ) ) { + update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) ); + } + + if ( isset( $data['exclude_sale_items'] ) ) { + update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); + } + + if ( isset( $data['minimum_amount'] ) ) { + update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); + } + + if ( isset( $data['maximum_amount'] ) ) { + update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); + } + + if ( isset( $data['customer_emails'] ) ) { + update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) ); + } + + do_action( 'woocommerce_api_edit_coupon', $id, $data ); + + return $this->get_coupon( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a coupon + * + * @since 2.2 + * @param int $id the coupon ID + * @param bool $force true to permanently delete coupon, false to move to trash + * @return array|WP_Error + */ + public function delete_coupon( $id, $force = false ) { + + $id = $this->validate_request( $id, 'shop_coupon', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + do_action( 'woocommerce_api_delete_coupon', $id, $this ); + + return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) ); + } + + /** + * expiry_date format + * + * @since 2.3.0 + * @param string $expiry_date + * @param bool $as_timestamp (default: false) + * @return string|int + */ + protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) { + if ( '' != $expiry_date ) { + if ( $as_timestamp ) { + return strtotime( $expiry_date ); + } + + return date( 'Y-m-d', strtotime( $expiry_date ) ); + } + + return ''; + } + + /** + * Helper method to get coupon post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_coupons( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'shop_coupon', + 'post_status' => 'publish', + ); + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Bulk update or insert coupons + * Accepts an array with coupons in the formats supported by + * WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['coupons'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 ); + } + + $data = $data['coupons']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $coupons = array(); + + foreach ( $data as $_coupon ) { + $coupon_id = 0; + + // Try to get the coupon ID + if ( isset( $_coupon['id'] ) ) { + $coupon_id = intval( $_coupon['id'] ); + } + + // Coupon exists / edit coupon + if ( $coupon_id ) { + $edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) ); + + if ( is_wp_error( $edit ) ) { + $coupons[] = array( + 'id' => $coupon_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $coupons[] = $edit['coupon']; + } + } else { + + // Coupon don't exists / create coupon + $new = $this->create_coupon( array( 'coupon' => $_coupon ) ); + + if ( is_wp_error( $new ) ) { + $coupons[] = array( + 'id' => $coupon_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $coupons[] = $new['coupon']; + } + } + } + + return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v2/class-wc-api-customers.php b/includes/api/legacy/v2/class-wc-api-customers.php new file mode 100644 index 00000000000..a38f2450d3b --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-customers.php @@ -0,0 +1,838 @@ + + * GET /customers//orders + * + * @since 2.2 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /customers + $routes[ $this->base ] = array( + array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ), + array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /customers/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ), + ); + + # GET/PUT/DELETE /customers/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), + array( array( $this, 'edit_customer' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), + array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ), + ); + + # GET /customers/email/ + $routes[ $this->base . '/email/(?P.+)' ] = array( + array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//downloads + $routes[ $this->base . '/(?P\d+)/downloads' ] = array( + array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ), + ); + + # POST|PUT /customers/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all customers + * + * @since 2.1 + * @param array $fields + * @param array $filter + * @param int $page + * @return array + */ + public function get_customers( $fields = null, $filter = array(), $page = 1 ) { + + $filter['page'] = $page; + + $query = $this->query_customers( $filter ); + + $customers = array(); + + foreach ( $query->get_results() as $user_id ) { + + if ( ! $this->is_readable( $user_id ) ) { + continue; + } + + $customers[] = current( $this->get_customer( $user_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'customers' => $customers ); + } + + /** + * Get the customer for the given ID + * + * @since 2.1 + * @param int $id the customer ID + * @param array $fields + * @return array|WP_Error + */ + public function get_customer( $id, $fields = null ) { + global $wpdb; + + $id = $this->validate_request( $id, 'customer', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $customer = new WC_Customer( $id ); + $last_order = $customer->get_last_order(); + $customer_data = array( + 'id' => $customer->get_id(), + 'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. + 'email' => $customer->get_email(), + 'first_name' => $customer->get_first_name(), + 'last_name' => $customer->get_last_name(), + 'username' => $customer->get_username(), + 'role' => $customer->get_role(), + 'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null, + 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times. + 'orders_count' => $customer->get_order_count(), + 'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ), + 'avatar_url' => $customer->get_avatar_url(), + 'billing_address' => array( + 'first_name' => $customer->get_billing_first_name(), + 'last_name' => $customer->get_billing_last_name(), + 'company' => $customer->get_billing_company(), + 'address_1' => $customer->get_billing_address_1(), + 'address_2' => $customer->get_billing_address_2(), + 'city' => $customer->get_billing_city(), + 'state' => $customer->get_billing_state(), + 'postcode' => $customer->get_billing_postcode(), + 'country' => $customer->get_billing_country(), + 'email' => $customer->get_billing_email(), + 'phone' => $customer->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $customer->get_shipping_first_name(), + 'last_name' => $customer->get_shipping_last_name(), + 'company' => $customer->get_shipping_company(), + 'address_1' => $customer->get_shipping_address_1(), + 'address_2' => $customer->get_shipping_address_2(), + 'city' => $customer->get_shipping_city(), + 'state' => $customer->get_shipping_state(), + 'postcode' => $customer->get_shipping_postcode(), + 'country' => $customer->get_shipping_country(), + ), + ); + + return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) ); + } + + /** + * Get the customer for the given email + * + * @since 2.1 + * + * @param string $email the customer email + * @param array $fields + * + * @return array|WP_Error + */ + public function get_customer_by_email( $email, $fields = null ) { + try { + if ( is_email( $email ) ) { + $customer = get_user_by( 'email', $email ); + if ( ! is_object( $customer ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 ); + } + } else { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 ); + } + + return $this->get_customer( $customer->ID, $fields ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of customers + * + * @since 2.1 + * + * @param array $filter + * + * @return array|WP_Error + */ + public function get_customers_count( $filter = array() ) { + try { + if ( ! current_user_can( 'list_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 ); + } + + $query = $this->query_customers( $filter ); + + return array( 'count' => $query->get_total() ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get customer billing address fields. + * + * @since 2.2 + * @return array + */ + protected function get_customer_billing_address() { + $billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array( + 'first_name', + 'last_name', + 'company', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + 'email', + 'phone', + ) ); + + return $billing_address; + } + + /** + * Get customer shipping address fields. + * + * @since 2.2 + * @return array + */ + protected function get_customer_shipping_address() { + $shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array( + 'first_name', + 'last_name', + 'company', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ) ); + + return $shipping_address; + } + + /** + * Add/Update customer data. + * + * @since 2.2 + * @param int $id the customer ID + * @param array $data + * @param WC_Customer $customer + */ + protected function update_customer_data( $id, $data, $customer ) { + + // Customer first name. + if ( isset( $data['first_name'] ) ) { + $customer->set_first_name( wc_clean( $data['first_name'] ) ); + } + + // Customer last name. + if ( isset( $data['last_name'] ) ) { + $customer->set_last_name( wc_clean( $data['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $data['billing_address'] ) ) { + foreach ( $this->get_customer_billing_address() as $field ) { + if ( isset( $data['billing_address'][ $field ] ) ) { + if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) { + $customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] ); + } else { + $customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) ); + } + } + } + } + + // Customer shipping address. + if ( isset( $data['shipping_address'] ) ) { + foreach ( $this->get_customer_shipping_address() as $field ) { + if ( isset( $data['shipping_address'][ $field ] ) ) { + if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { + $customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] ); + } else { + $customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) ); + } + } + } + } + + do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer ); + } + + /** + * Create a customer + * + * @since 2.2 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_customer( $data ) { + try { + if ( ! isset( $data['customer'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 ); + } + + $data = $data['customer']; + + // Checks with can create new users. + if ( ! current_user_can( 'create_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this ); + + // Checks with the email is missing. + if ( ! isset( $data['email'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 ); + } + + // Create customer. + $customer = new WC_Customer; + $customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' ); + $customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' ); + $customer->set_email( $data['email'] ); + $customer->save(); + + if ( ! $customer->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); + } + + // Added customer data. + $this->update_customer_data( $customer->get_id(), $data, $customer ); + $customer->save(); + + do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data ); + + $this->server->send_status( 201 ); + + return $this->get_customer( $customer->get_id() ); + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a customer + * + * @since 2.2 + * + * @param int $id the customer ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_customer( $id, $data ) { + try { + if ( ! isset( $data['customer'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 ); + } + + $data = $data['customer']; + + // Validate the customer ID. + $id = $this->validate_request( $id, 'customer', 'edit' ); + + // Return the validate error. + if ( is_wp_error( $id ) ) { + throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this ); + + $customer = new WC_Customer( $id ); + + // Customer email. + if ( isset( $data['email'] ) ) { + $customer->set_email( $data['email'] ); + } + + // Customer password. + if ( isset( $data['password'] ) ) { + $customer->set_password( $data['password'] ); + } + + // Update customer data. + $this->update_customer_data( $customer->get_id(), $data, $customer ); + + $customer->save(); + + do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data ); + + return $this->get_customer( $customer->get_id() ); + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a customer + * + * @since 2.2 + * @param int $id the customer ID + * @return array|WP_Error + */ + public function delete_customer( $id ) { + + // Validate the customer ID. + $id = $this->validate_request( $id, 'customer', 'delete' ); + + // Return the validate error. + if ( is_wp_error( $id ) ) { + return $id; + } + + do_action( 'woocommerce_api_delete_customer', $id, $this ); + + return $this->delete( $id, 'customer' ); + } + + /** + * Get the orders for a customer + * + * @since 2.1 + * @param int $id the customer ID + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_customer_orders( $id, $fields = null ) { + global $wpdb; + + $id = $this->validate_request( $id, 'customer', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order_ids = wc_get_orders( array( + 'customer' => $id, + 'limit' => -1, + 'orderby' => 'date', + 'order' => 'ASC', + 'return' => 'ids', + ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $orders = array(); + + foreach ( $order_ids as $order_id ) { + $orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) ); + } + + return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) ); + } + + /** + * Get the available downloads for a customer + * + * @since 2.2 + * @param int $id the customer ID + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_customer_downloads( $id, $fields = null ) { + $id = $this->validate_request( $id, 'customer', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $downloads = array(); + $_downloads = wc_get_customer_available_downloads( $id ); + + foreach ( $_downloads as $key => $download ) { + $downloads[] = array( + 'download_url' => $download['download_url'], + 'download_id' => $download['download_id'], + 'product_id' => $download['product_id'], + 'download_name' => $download['download_name'], + 'order_id' => $download['order_id'], + 'order_key' => $download['order_key'], + 'downloads_remaining' => $download['downloads_remaining'], + 'access_expires' => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null, + 'file' => $download['file'], + ); + } + + return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) ); + } + + /** + * Helper method to get customer user objects + * + * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited + * pagination support + * + * The filter for role can only be a single role in a string. + * + * @since 2.3 + * @param array $args request arguments for filtering query + * @return WP_User_Query + */ + private function query_customers( $args = array() ) { + + // default users per page + $users_per_page = get_option( 'posts_per_page' ); + + // Set base query arguments + $query_args = array( + 'fields' => 'ID', + 'role' => 'customer', + 'orderby' => 'registered', + 'number' => $users_per_page, + ); + + // Custom Role + if ( ! empty( $args['role'] ) ) { + $query_args['role'] = $args['role']; + } + + // Search + if ( ! empty( $args['q'] ) ) { + $query_args['search'] = $args['q']; + } + + // Limit number of users returned + if ( ! empty( $args['limit'] ) ) { + if ( -1 == $args['limit'] ) { + unset( $query_args['number'] ); + } else { + $query_args['number'] = absint( $args['limit'] ); + $users_per_page = absint( $args['limit'] ); + } + } else { + $args['limit'] = $query_args['number']; + } + + // Page + $page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1; + + // Offset + if ( ! empty( $args['offset'] ) ) { + $query_args['offset'] = absint( $args['offset'] ); + } else { + $query_args['offset'] = $users_per_page * ( $page - 1 ); + } + + // Created date + if ( ! empty( $args['created_at_min'] ) ) { + $this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] ); + } + + if ( ! empty( $args['created_at_max'] ) ) { + $this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] ); + } + + // Order (ASC or DESC, ASC by default) + if ( ! empty( $args['order'] ) ) { + $query_args['order'] = $args['order']; + } + + // Orderby + if ( ! empty( $args['orderby'] ) ) { + $query_args['orderby'] = $args['orderby']; + + // Allow sorting by meta value + if ( ! empty( $args['orderby_meta_key'] ) ) { + $query_args['meta_key'] = $args['orderby_meta_key']; + } + } + + $query = new WP_User_Query( $query_args ); + + // Helper members for pagination headers + $query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page ); + $query->page = $page; + + return $query; + } + + /** + * Add customer data to orders + * + * @since 2.1 + * @param $order_data + * @param $order + * @return array + */ + public function add_customer_data( $order_data, $order ) { + + if ( 0 == $order->get_user_id() ) { + + // add customer data from order + $order_data['customer'] = array( + 'id' => 0, + 'email' => $order->get_billing_email(), + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + ); + + } else { + + $order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) ); + } + + return $order_data; + } + + /** + * Modify the WP_User_Query to support filtering on the date the customer was created + * + * @since 2.1 + * @param WP_User_Query $query + */ + public function modify_user_query( $query ) { + + if ( $this->created_at_min ) { + $query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) ); + } + + if ( $this->created_at_max ) { + $query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) ); + } + } + + /** + * Validate the request by checking: + * + * 1) the ID is a valid integer + * 2) the ID returns a valid WP_User + * 3) the current user has the proper permissions + * + * @since 2.1 + * @see WC_API_Resource::validate_request() + * @param integer $id the customer ID + * @param string $type the request type, unused because this method overrides the parent class + * @param string $context the context of the request, either `read`, `edit` or `delete` + * @return int|WP_Error valid user ID or WP_Error if any of the checks fails + */ + protected function validate_request( $id, $type, $context ) { + + try { + $id = absint( $id ); + + // validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 ); + } + + // non-existent IDs return a valid WP_User object with the user ID = 0 + $customer = new WP_User( $id ); + + if ( 0 === $customer->ID ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 ); + } + + // validate permissions + switch ( $context ) { + + case 'read': + if ( ! current_user_can( 'list_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 ); + } + break; + + case 'edit': + if ( ! current_user_can( 'edit_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 ); + } + break; + + case 'delete': + if ( ! current_user_can( 'delete_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 ); + } + break; + } + + return $id; + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Check if the current user can read users + * + * @since 2.1 + * @see WC_API_Resource::is_readable() + * @param int|WP_Post $post unused + * @return bool true if the current user can read users, false otherwise + */ + protected function is_readable( $post ) { + return current_user_can( 'list_users' ); + } + + /** + * Bulk update or insert customers + * Accepts an array with customers in the formats supported by + * WC_API_Customers->create_customer() and WC_API_Customers->edit_customer() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['customers'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 ); + } + + $data = $data['customers']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $customers = array(); + + foreach ( $data as $_customer ) { + $customer_id = 0; + + // Try to get the customer ID + if ( isset( $_customer['id'] ) ) { + $customer_id = intval( $_customer['id'] ); + } + + // Customer exists / edit customer + if ( $customer_id ) { + $edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) ); + + if ( is_wp_error( $edit ) ) { + $customers[] = array( + 'id' => $customer_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $customers[] = $edit['customer']; + } + } else { + // Customer don't exists / create customer + $new = $this->create_customer( array( 'customer' => $_customer ) ); + + if ( is_wp_error( $new ) ) { + $customers[] = array( + 'id' => $customer_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $customers[] = $new['customer']; + } + } + } + + return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v2/class-wc-api-exception.php b/includes/api/legacy/v2/class-wc-api-exception.php new file mode 100644 index 00000000000..834ed04d6eb --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-exception.php @@ -0,0 +1,48 @@ +error_code = $error_code; + parent::__construct( $error_message, $http_status_code ); + } + + /** + * Returns the error code + * + * @since 2.2 + * @return string + */ + public function getErrorCode() { + return $this->error_code; + } +} diff --git a/includes/api/legacy/v2/class-wc-api-json-handler.php b/includes/api/legacy/v2/class-wc-api-json-handler.php new file mode 100644 index 00000000000..e19ac15d371 --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-json-handler.php @@ -0,0 +1,78 @@ +api->server->send_status( 400 ); + + $data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ); + } + + // Check for invalid characters (only alphanumeric allowed) + if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) { + + WC()->api->server->send_status( 400 ); + + $data = array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ); + } + + // see http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ + WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); + + // Prepend '/**/' to mitigate possible JSONP Flash attacks + return '/**/' . $_GET['_jsonp'] . '(' . json_encode( $data ) . ')'; + } + + return json_encode( $data ); + } +} diff --git a/includes/api/legacy/v2/class-wc-api-orders.php b/includes/api/legacy/v2/class-wc-api-orders.php new file mode 100644 index 00000000000..d6c0a1e37ac --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-orders.php @@ -0,0 +1,1834 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET|POST /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET /orders/statuses + $routes[ $this->base . '/statuses' ] = array( + array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /orders/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_order' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), + ); + + # GET|POST /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//notes/ + $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( + array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), + ); + + # GET|POST /orders//refunds + $routes[ $this->base . '/(?P\d+)/refunds' ] = array( + array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//refunds/ + $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( + array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /orders/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all orders + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param string $status + * @param int $page + * @return array + */ + public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_orders( $filter ); + + $orders = array(); + + foreach ( $query->posts as $order_id ) { + + if ( ! $this->is_readable( $order_id ) ) { + continue; + } + + $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'orders' => $orders ); + } + + + /** + * Get the order for the given ID + * + * @since 2.1 + * @param int $id the order ID + * @param array $fields + * @param array $filter + * @return array|WP_Error + */ + public function get_order( $id, $fields = null, $filter = array() ) { + + // ensure order ID is valid & user has permission to read + $id = $this->validate_request( $id, $this->post_type, 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + // Get the decimal precession + $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); + $order = wc_get_order( $id ); + $order_data = array( + 'id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'status' => $order->get_status(), + 'currency' => $order->get_currency(), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'shipping_methods' => $order->get_shipping_method(), + 'payment_details' => array( + 'method_id' => $order->get_payment_method(), + 'method_title' => $order->get_payment_method_title(), + 'paid' => ! is_null( $order->get_date_paid() ), + ), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + 'note' => $order->get_customer_note(), + 'customer_ip' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'customer_id' => $order->get_user_id(), + 'view_order_url' => $order->get_view_order_url(), + 'line_items' => array(), + 'shipping_lines' => array(), + 'tax_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // add line items + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $order_data['line_items'][] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + ); + } + + // add shipping + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $order_data['shipping_lines'][] = array( + 'id' => $shipping_item_id, + 'method_id' => $shipping_item->get_method_id(), + 'method_title' => $shipping_item->get_name(), + 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), + ); + } + + // add taxes + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $order_data['tax_lines'][] = array( + 'id' => $tax->id, + 'rate_id' => $tax->rate_id, + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, $dp ), + 'compound' => (bool) $tax->is_compound, + ); + } + + // add fees + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $order_data['fee_lines'][] = array( + 'id' => $fee_item_id, + 'title' => $fee_item->get_name(), + 'tax_class' => $fee_item->get_tax_class(), + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + ); + } + + // add coupons + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $order_data['coupon_lines'][] = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item->get_code(), + 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), + ); + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.4 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + try { + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + + if ( 'any' === $status ) { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $filter['status'] = str_replace( 'wc-', '', $slug ); + $query = $this->query_orders( $filter ); + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; + } + + return array( 'count' => $order_statuses ); + + } else { + $filter['status'] = $status; + } + } + + $query = $this->query_orders( $filter ); + + return array( 'count' => (int) $query->found_posts ); + + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a list of valid order statuses + * + * Note this requires no specific permissions other than being an authenticated + * API user. Order statuses (particularly custom statuses) could be considered + * private information which is why it's not in the API index. + * + * @since 2.1 + * @return array + */ + public function get_order_statuses() { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; + } + + return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); + } + + /** + * Create an order + * + * @since 2.2 + * + * @param array $data raw order data + * + * @return array|WP_Error + */ + public function create_order( $data ) { + global $wpdb; + + wc_transaction_query( 'start' ); + + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); + + // default order args, note that status is checked for validity in wc_create_order() + $default_order_args = array( + 'status' => isset( $data['status'] ) ? $data['status'] : '', + 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, + ); + + // if creating order for existing customer + if ( ! empty( $data['customer_id'] ) ) { + + // make sure customer exists + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $default_order_args['customer_id'] = $data['customer_id']; + } + + // create the pending order + $order = $this->create_base_order( $default_order_args, $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // billing/shipping addresses + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $set_item = "set_{$line_type}"; + + foreach ( $data[ $line ] as $item ) { + + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // calculate totals and set them + $order->calculate_totals(); + + // payment method (and payment_complete() if `paid` == true) + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // method ID & title are required + if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + update_post_meta( $order->get_id(), '_payment_method_title', $data['payment_details']['method_title'] ); + + // mark as paid if set + if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // set order currency + if ( isset( $data['currency'] ) ) { + + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // set order meta + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); + + wc_transaction_query( 'commit' ); + + return $this->get_order( $order->get_id() ); + + } catch ( WC_Data_Exception $e ) { + wc_transaction_query( 'rollback' ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + wc_transaction_query( 'rollback' ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Creates new WC_Order. + * + * Requires a separate function for classes that extend WC_API_Orders. + * + * @since 2.3 + * + * @param $args array + * @param $data + * + * @return WC_Order + */ + protected function create_base_order( $args, $data ) { + return wc_create_order( $args ); + } + + /** + * Edit an order + * + * @since 2.2 + * + * @param int $id the order ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + $update_totals = false; + + $id = $this->validate_request( $id, $this->post_type, 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); + $order = wc_get_order( $id ); + + if ( empty( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $order_args = array( 'order_id' => $order->get_id() ); + + // Customer note. + if ( isset( $data['note'] ) ) { + $order_args['customer_note'] = $data['note']; + } + + // Customer ID. + if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); + } + + // Billing/shipping address. + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $update_totals = true; + + foreach ( $data[ $line ] as $item ) { + + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Payment method (and payment_complete() if `paid` == true and order needs payment). + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // Method ID. + if ( isset( $data['payment_details']['method_id'] ) ) { + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + } + + // Method title. + if ( isset( $data['payment_details']['method_title'] ) ) { + update_post_meta( $order->get_id(), '_payment_method_title', $data['payment_details']['method_title'] ); + } + + // Mark as paid if set. + if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // Set order currency. + if ( isset( $data['currency'] ) ) { + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update order meta. + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $data['status'] ) ) { + // Refresh the order instance. + $order = wc_get_order( $order->get_id() ); + $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); + } + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); + + return $this->get_order( $id ); + + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, $this->post_type, 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + wc_delete_shop_order_transients( $id ); + + do_action( 'woocommerce_api_delete_order', $id, $this ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + protected function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => $this->post_type, + 'post_status' => array_keys( wc_get_order_statuses() ), + ); + + // add status argument + if ( ! empty( $args['status'] ) ) { + + $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); + $statuses = explode( ',', $statuses ); + $query_args['post_status'] = $statuses; + + unset( $args['status'] ); + + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Helper method to set/update the billing & shipping addresses for + * an order + * + * @since 2.1 + * @param \WC_Order $order + * @param array $data + */ + protected function set_order_addresses( $order, $data ) { + + $address_fields = array( + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ); + + $billing_address = $shipping_address = array(); + + // billing address + if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['billing_address'][ $field ] ) ) { + $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); + } + } + + unset( $address_fields['email'] ); + unset( $address_fields['phone'] ); + } + + // shipping address + if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['shipping_address'][ $field ] ) ) { + $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); + } + } + } + + $this->update_address( $order, $billing_address, 'billing' ); + $this->update_address( $order, $shipping_address, 'shipping' ); + + // update user meta + if ( $order->get_user_id() ) { + foreach ( $billing_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); + } + foreach ( $shipping_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); + } + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Helper method to add/update order meta, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @since 2.2 + * @param int $order_id valid order ID + * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format + */ + protected function set_order_meta( $order_id, $order_meta ) { + + foreach ( $order_meta as $meta_key => $meta_value ) { + + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null + * + * Items can be deleted by setting the resource ID to null + * + * @since 2.2 + * @param array $item item provided in the request body + * @return bool true if the item resource ID is null, false otherwise + */ + protected function item_is_null( $item ) { + + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Wrapper method to create/update order items + * + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @since 2.2 + * @param \WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_API_Exception if item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = "set_{$item_type}"; + + // verify provided line item ID is associated with order + if ( 'update' === $action ) { + + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->get_id() ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Create or update a line item + * + * @since 2.2 + * @param \WC_Order $order + * @param array $item line item data + * @param string $action 'create' to add line item or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_line_item( $order, $item, $action ) { + $creating = ( 'create' === $action ); + + // product is always required + if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); + } + + // when updating, ensure product ID provided matches + if ( 'update' === $action ) { + + $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); + } + } + + if ( isset( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } elseif ( isset( $item['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $item['sku'] ); + } + + // variations must each have a key & value + $variation_id = 0; + if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { + foreach ( $item['variations'] as $key => $value ) { + if ( ! $key || ! $value ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); + } + } + $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); + } + + $product = wc_get_product( $variation_id ? $variation_id : $product_id ); + + // must be a valid WC_Product + if ( ! is_object( $product ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // quantity must be positive float + if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // quantity is required when creating + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + if ( $creating ) { + $line_item = new WC_Order_Item_Product(); + } else { + $line_item = new WC_Order_Item_Product( $item['id'] ); + } + + $line_item->set_product( $product ); + $line_item->set_order_id( $order->get_id() ); + + if ( isset( $item['quantity'] ) ) { + $line_item->set_quantity( $item['quantity'] ); + } + if ( isset( $item['total'] ) ) { + $line_item->set_total( floatval( $item['total'] ) ); + } elseif ( $creating ) { + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); + $line_item->set_total( $total ); + $line_item->set_subtotal( $total ); + } + if ( isset( $item['total_tax'] ) ) { + $line_item->set_total_tax( floatval( $item['total_tax'] ) ); + } + if ( isset( $item['subtotal'] ) ) { + $line_item->set_subtotal( floatval( $item['subtotal'] ) ); + } + if ( isset( $item['subtotal_tax'] ) ) { + $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); + } + if ( $variation_id ) { + $line_item->set_variation_id( $variation_id ); + $line_item->set_variation( $item['variations'] ); + } + + // Save or add to order. + if ( $creating ) { + $order->add_item( $line_item ); + } else { + $item_id = $line_item->save(); + + if ( ! $item_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Given a product ID & API provided variations, find the correct variation ID to use for calculation + * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass + * the cheapest variation ID but provide other information so we have to look up the variation ID. + * + * @param WC_Product $product + * @param array $variations + * + * @return int returns an ID if a valid variation was found for this product + */ + function get_variation_id( $product, $variations = array() ) { + $variation_id = null; + $variations_normalized = array(); + + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + if ( isset( $variations ) && is_array( $variations ) ) { + // start by normalizing the passed variations + foreach ( $variations as $key => $value ) { + $key = str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ); // from get_attributes in class-wc-api-products.php + $variations_normalized[ $key ] = strtolower( $value ); + } + // now search through each product child and see if our passed variations match anything + foreach ( $product->get_children() as $variation ) { + $meta = array(); + foreach ( get_post_meta( $variation ) as $key => $value ) { + $value = $value[0]; + $key = str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ); + $meta[ $key ] = strtolower( $value ); + } + // if the variation array is a part of the $meta array, we found our match + if ( $this->array_contains( $variations_normalized, $meta ) ) { + $variation_id = $variation; + break; + } + } + } + } + + return $variation_id; + } + + /** + * Utility function to see if the meta array contains data from variations + * + * @param array $needles + * @param array $haystack + * + * @return bool + */ + protected function array_contains( $needles, $haystack ) { + foreach ( $needles as $key => $value ) { + if ( $haystack[ $key ] !== $value ) { + return false; + } + } + return true; + } + + /** + * Create or update an order shipping method + * + * @since 2.2 + * @param \WC_Order $order + * @param array $shipping item data + * @param string $action 'create' to add shipping or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_shipping( $order, $shipping, $action ) { + + // total must be a positive float + if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // method ID is required + if ( ! isset( $shipping['method_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + $item = new WC_Order_Item_Shipping(); + $item->set_order_id( $order->get_id() ); + $item->set_shipping_rate( $rate ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Shipping( $shipping['id'] ); + + if ( isset( $shipping['method_id'] ) ) { + $item->set_method_id( $shipping['method_id'] ); + } + + if ( isset( $shipping['method_title'] ) ) { + $item->set_method_title( $shipping['method_title'] ); + } + + if ( isset( $shipping['total'] ) ) { + $item->set_total( floatval( $shipping['total'] ) ); + } + + $shipping_id = $item->save(); + + if ( ! $shipping_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee + * + * @since 2.2 + * @param \WC_Order $order + * @param array $fee item data + * @param string $action 'create' to add fee or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_fee( $order, $fee, $action ) { + + if ( 'create' === $action ) { + + // fee title is required + if ( ! isset( $fee['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order->get_id() ); + $item->set_name( wc_clean( $fee['title'] ) ); + $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); + + // if taxable, tax class and total are required + if ( ! empty( $fee['taxable'] ) ) { + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $item->set_tax_status( 'taxable' ); + $item->set_tax_class( $fee['tax_class'] ); + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); + } + + if ( isset( $fee['tax_data'] ) ) { + $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); + $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); + } + } + + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Fee( $fee['id'] ); + + if ( isset( $fee['title'] ) ) { + $item->set_name( wc_clean( $fee['title'] ) ); + } + + if ( isset( $fee['tax_class'] ) ) { + $item->set_tax_class( $fee['tax_class'] ); + } + + if ( isset( $fee['total'] ) ) { + $item->set_total( floatval( $fee['total'] ) ); + } + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( floatval( $fee['total_tax'] ) ); + } + + $fee_id = $item->save(); + + if ( ! $fee_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon + * + * @since 2.2 + * @param \WC_Order $order + * @param array $coupon item data + * @param string $action 'create' to add coupon or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_coupon( $order, $coupon, $action ) { + + // coupon amount must be positive float + if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // coupon code is required + if ( empty( $coupon['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $coupon['code'], + 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, + 'discount_tax' => 0, + 'order_id' => $order->get_id(), + ) ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Coupon( $coupon['id'] ); + + if ( isset( $coupon['code'] ) ) { + $item->set_code( $coupon['code'] ); + } + + if ( isset( $coupon['amount'] ) ) { + $item->set_discount( floatval( $coupon['amount'] ) ); + } + + $coupon_id = $item->save(); + + if ( ! $coupon_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $order_id, $fields = null ) { + + // ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $args = array( + 'post_id' => $order_id, + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $order_notes = array(); + + foreach ( $notes as $note ) { + + $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); + } + + /** + * Get an order note for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param string $id order note ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_note( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $order_note = array( + 'id' => $note->comment_ID, + 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false, + ); + + return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order note for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @return WP_Error|array error or created note response data + */ + public function create_order_note( $order_id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); + } + + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); + + // note content is required + if ( ! isset( $data['note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); + } + + $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); + + // create the note + $note_id = $order->add_order_note( $data['note'], $is_customer_note ); + + if ( ! $note_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); + + return $this->get_order_note( $order->get_id(), $note_id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit the order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @param array $data parsed request data + * @return WP_Error|array error or edited note response data + */ + public function edit_order_note( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); + + // Note content + if ( isset( $data['note'] ) ) { + + wp_update_comment( + array( + 'comment_ID' => $note->comment_ID, + 'comment_content' => $data['note'], + ) + ); + } + + // Customer note + if ( isset( $data['customer_note'] ) ) { + + update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); + } + + do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); + + return $this->get_order_note( $order->get_id(), $note->comment_ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_note( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + // Force delete since trashed order notes could not be managed through comments list table + $result = wp_delete_comment( $note->comment_ID, true ); + + if ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); + } + + do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); + + return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the order refunds for an order + * + * @since 2.2 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_refunds( $order_id, $fields = null ) { + + // Ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $refund_items = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $order_id, + 'limit' => -1, + 'return' => 'ids', + ) ); + $order_refunds = array(); + + foreach ( $refund_items as $refund_id ) { + $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); + } + + return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); + } + + /** + * Get an order refund for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param int $id + * @param string $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_refund( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + $order = wc_get_order( $order_id ); + $refund = wc_get_order( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + $line_items = array(); + + // Add line items + foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_items[] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), + 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), + 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), + ); + } + + $order_refund = array( + 'id' => $refund->get_id(), + 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), + 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), + 'reason' => $refund->get_reason(), + 'line_items' => $line_items, + ); + + return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order refund for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @param bool $api_refund do refund using a payment gateway API + * @return WP_Error|array error or created refund response data + */ + public function create_order_refund( $order_id, $data, $api_refund = true ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); + } + + $order_id = absint( $order_id ); + + if ( empty( $order_id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); + + // Refund amount is required + if ( ! isset( $data['amount'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); + } elseif ( 0 > $data['amount'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); + } + + $data['order_id'] = $order_id; + $data['refund_id'] = 0; + + // Create the refund + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_id ); + + if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit an order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @param array $data parsed request data + * @return WP_Error|array error or edited refund response data + */ + public function edit_order_refund( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure order ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); + + // Update reason + if ( isset( $data['reason'] ) ) { + $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); + + if ( is_wp_error( $updated_refund ) ) { + return $updated_refund; + } + } + + // Update refund amount + if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { + update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); + } + + do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_refund( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure refund ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + wc_delete_shop_order_transients( $order_id ); + + do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); + + return $this->delete( $refund->ID, 'refund', true ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Bulk update or insert orders + * Accepts an array with orders in the formats supported by + * WC_API_Orders->create_order() and WC_API_Orders->edit_order() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['orders'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); + } + + $data = $data['orders']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $orders = array(); + + foreach ( $data as $_order ) { + $order_id = 0; + + // Try to get the order ID + if ( isset( $_order['id'] ) ) { + $order_id = intval( $_order['id'] ); + } + + // Order exists / edit order + if ( $order_id ) { + $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); + + if ( is_wp_error( $edit ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $orders[] = $edit['order']; + } + } else { + // Order don't exists / create order + $new = $this->create_order( array( 'order' => $_order ) ); + + if ( is_wp_error( $new ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $orders[] = $new['order']; + } + } + } + + return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v2/class-wc-api-products.php b/includes/api/legacy/v2/class-wc-api-products.php new file mode 100644 index 00000000000..456effd95e5 --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-products.php @@ -0,0 +1,2363 @@ + + * GET /products//reviews + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /products + $routes[ $this->base ] = array( + array( array( $this, 'get_products' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /products/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /products/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_product' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), + ); + + # GET /products//reviews + $routes[ $this->base . '/(?P\d+)/reviews' ] = array( + array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), + ); + + # GET /products//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), + ); + + # GET /products/categories + $routes[ $this->base . '/categories' ] = array( + array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), + ); + + # GET /products/categories/ + $routes[ $this->base . '/categories/(?P\d+)' ] = array( + array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), + ); + + # GET/POST /products/attributes + $routes[ $this->base . '/attributes' ] = array( + array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /attributes/ + $routes[ $this->base . '/attributes/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), + ); + + # GET /products/sku/ + $routes[ $this->base . '/sku/(?P\w[\w\s\-]*)' ] = array( + array( array( $this, 'get_product_by_sku' ), WC_API_Server::READABLE ), + ); + + # POST|PUT /products/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all products + * + * @since 2.1 + * @param string $fields + * @param string $type + * @param array $filter + * @param int $page + * @return array + */ + public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $filter['page'] = $page; + + $query = $this->query_products( $filter ); + + $products = array(); + + foreach ( $query->posts as $product_id ) { + + if ( ! $this->is_readable( $product_id ) ) { + continue; + } + + $products[] = current( $this->get_product( $product_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'products' => $products ); + } + + /** + * Get the product for the given ID + * + * @since 2.1 + * @param int $id the product ID + * @param string $fields + * @return array|WP_Error + */ + public function get_product( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + // add data that applies to every product type + $product_data = $this->get_product_data( $product ); + + // add variations to variable products + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $product_data['variations'] = $this->get_variation_data( $product ); + } + + // add the parent product data to an individual variation + if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) { + $_product = wc_get_product( $product->get_parent_id() ); + $product_data['parent'] = $this->get_product_data( $_product ); + } + + return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); + } + + /** + * Get the total number of products + * + * @since 2.1 + * + * @param string $type + * @param array $filter + * + * @return array|WP_Error + */ + public function get_products_count( $type = null, $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $query = $this->query_products( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product + * + * @since 2.2 + * + * @param array $data posted data + * + * @return array|WP_Error + */ + public function create_product( $data ) { + $id = 0; + + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + // Check permissions + if ( ! current_user_can( 'publish_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); + + // Check if product title is specified + if ( ! isset( $data['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); + } + + // Check product type + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'simple'; + } + + // Set visible visibility when not sent + if ( ! isset( $data['catalog_visibility'] ) ) { + $data['catalog_visibility'] = 'visible'; + } + + // Validate the product type + if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Enable description html tags. + $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; + if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { + + $post_content = $data['description']; + } + + // Enable short description html tags. + $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; + if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { + $post_excerpt = $data['short_description']; + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + $product = new $classname(); + + $product->set_name( wc_clean( $data['title'] ) ); + $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); + $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); + $product->set_description( isset( $data['description'] ) ? $post_content : '' ); + + // Attempts to create the new product. + $product->save(); + $id = $product->get_id(); + + // Checks for an error in the product creation + if ( 0 >= $id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); + } + + // Check for featured/gallery images, upload it and set it + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields + $product = $this->save_product_meta( $product, $data ); + $product->save(); + + // Save variations + if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } + + do_action( 'woocommerce_api_create_product', $id, $data ); + + // Clear cache/transients + wc_delete_product_transients( $id ); + + $this->server->send_status( 201 ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product + * + * @since 2.2 + * + * @param int $id the product ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product( $id, $data ) { + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + $id = $this->validate_request( $id, 'product', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); + + // Product title. + if ( isset( $data['title'] ) ) { + $product->set_name( wc_clean( $data['title'] ) ); + } + + // Product name (slug). + if ( isset( $data['name'] ) ) { + $product->set_slug( wc_clean( $data['name'] ) ); + } + + // Product status. + if ( isset( $data['status'] ) ) { + $product->set_status( wc_clean( $data['status'] ) ); + } + + // Product short description. + if ( isset( $data['short_description'] ) ) { + // Enable short description html tags. + $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? $data['short_description'] : wc_clean( $data['short_description'] ); + $product->set_short_description( $post_excerpt ); + } + + // Product description. + if ( isset( $data['description'] ) ) { + // Enable description html tags. + $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? $data['description'] : wc_clean( $data['description'] ); + $product->set_description( $post_content ); + } + + // Validate the product type. + if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } else { + // Just sync variations. + $product = WC_Product_Variable::sync( $product, false ); + } + } + + $product->save(); + + do_action( 'woocommerce_api_edit_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product. + * + * @since 2.2 + * + * @param int $id the product ID. + * @param bool $force true to permanently delete order, false to move to trash. + * + * @return array|WP_Error + */ + public function delete_product( $id, $force = false ) { + + $id = $this->validate_request( $id, 'product', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + do_action( 'woocommerce_api_delete_product', $id, $this ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->delete( true ); + } + } elseif ( $product->is_type( 'grouped' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->set_parent_id( 0 ); + $child->save(); + } + } + + $product->delete( true ); + $result = $product->get_id() > 0 ? false : true; + } else { + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); + } else { + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); + } + } + + /** + * Get the reviews for a product + * + * @since 2.1 + * @param int $id the product ID to get reviews for + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_product_reviews( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $comments = get_approved_comments( $id ); + $reviews = array(); + + foreach ( $comments as $comment ) { + + $reviews[] = array( + 'id' => intval( $comment->comment_ID ), + 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), + 'review' => $comment->comment_content, + 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), + 'reviewer_name' => $comment->comment_author, + 'reviewer_email' => $comment->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), + ); + } + + return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); + } + + /** + * Get the orders for a product + * + * @since 2.4.0 + * @param int $id the product ID to get orders for + * @param string fields fields to retrieve + * @param array $filter filters to include in response + * @param string $status the order status to retrieve + * @param $page $page page to retrieve + * @return array|WP_Error + */ + public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { + global $wpdb; + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $id ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $filter = array_merge( $filter, array( + 'in' => implode( ',', $order_ids ), + ) ); + + $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); + + return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); + } + + /** + * Get a listing of product categories + * + * @since 2.2 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_categories( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $product_categories = array(); + + $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); + } + + return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product category for the given ID + * + * @since 2.2 + * + * @param string $id product category term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_category( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_cat' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + // Get category display type + $display_type = get_woocommerce_term_meta( $term_id, 'display_type' ); + + // Get category image + $image = ''; + if ( $image_id = get_woocommerce_term_meta( $term_id, 'thumbnail_id' ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $product_category = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => $image ? esc_url( $image ) : '', + 'count' => intval( $term->count ), + ); + + return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Helper method to get product post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_products( $args ) { + + // Set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'product', + 'post_status' => 'publish', + 'meta_query' => array(), + ); + + if ( ! empty( $args['type'] ) ) { + + $types = explode( ',', $args['type'] ); + + $query_args['tax_query'] = array( + array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $types, + ), + ); + + unset( $args['type'] ); + } + + // Filter products by category + if ( ! empty( $args['category'] ) ) { + $query_args['product_cat'] = $args['category']; + } + + // Filter by specific sku + if ( ! empty( $args['sku'] ) ) { + if ( ! is_array( $query_args['meta_query'] ) ) { + $query_args['meta_query'] = array(); + } + + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $args['sku'], + 'compare' => '=', + ); + + $query_args['post_type'] = array( 'product', 'product_variation' ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Get standard product data that applies to every product type + * + * @since 2.1 + * @param WC_Product|int $product + * @return array + */ + private function get_product_data( $product ) { + if ( is_numeric( $product ) ) { + $product = wc_get_product( $product ); + } + + $prices_precision = wc_get_price_decimals(); + return array( + 'title' => $product->get_name(), + 'id' => $product->get_id(), + 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'downloadable' => $product->is_downloadable(), + 'virtual' => $product->is_virtual(), + 'permalink' => $product->get_permalink(), + 'sku' => $product->get_sku(), + 'price' => wc_format_decimal( $product->get_price(), $prices_precision ), + 'regular_price' => wc_format_decimal( $product->get_regular_price(), $prices_precision ), + 'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), $prices_precision ) : null, + 'price_html' => $product->get_price_html(), + 'taxable' => $product->is_taxable(), + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'managing_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'purchaseable' => $product->is_purchasable(), + 'featured' => $product->is_featured(), + 'visible' => $product->is_visible(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'on_sale' => $product->is_on_sale(), + 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null, + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, + 'description' => wpautop( do_shortcode( $product->get_description() ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'parent_id' => $product->get_parent_id(), + 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), + 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), + 'images' => $this->get_images( $product ), + 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), + 'attributes' => $this->get_attributes( $product ), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'download_type' => 'standard', + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'total_sales' => $product->get_total_sales(), + 'variations' => array(), + 'parent' => array(), + ); + } + + /** + * Get an individual variation's data + * + * @since 2.1 + * @param WC_Product $product + * @return array + */ + private function get_variation_data( $product ) { + $prices_precision = wc_get_price_decimals(); + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + + $variation = wc_get_product( $child_id ); + + if ( ! $variation || ! $variation->exists() ) { + continue; + } + + $variations[] = array( + 'id' => $variation->get_id(), + 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), + 'downloadable' => $variation->is_downloadable(), + 'virtual' => $variation->is_virtual(), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => wc_format_decimal( $variation->get_price(), $prices_precision ), + 'regular_price' => wc_format_decimal( $variation->get_regular_price(), $prices_precision ), + 'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), $prices_precision ) : null, + 'taxable' => $variation->is_taxable(), + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'managing_stock' => $variation->managing_stock(), + 'stock_quantity' => (int) $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backordered' => $variation->is_on_backorder(), + 'purchaseable' => $variation->is_purchasable(), + 'visible' => $variation->variation_is_visible(), + 'on_sale' => $variation->is_on_sale(), + 'weight' => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null, + 'dimensions' => array( + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $product->get_download_limit(), + 'download_expiry' => (int) $product->get_download_expiry(), + ); + } + + return $variations; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * @param WC_Product $product + * @param array $request + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { + if ( ! isset( $default_attr['name'] ) ) { + continue; + } + + $taxonomy = sanitize_title( $default_attr['name'] ); + + if ( isset( $default_attr['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + + if ( $_attribute['is_variation'] ) { + $value = ''; + + if ( isset( $default_attr['option'] ) ) { + if ( $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters. + $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); + } else { + $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); + } + } + + if ( $value ) { + $default_attributes[ $taxonomy ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Save product meta + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + * @throws WC_API_Exception + */ + protected function save_product_meta( $product, $data ) { + global $wpdb; + + // Virtual + if ( isset( $data['virtual'] ) ) { + $product->set_virtual( $data['virtual'] ); + } + + // Tax status + if ( isset( $data['tax_status'] ) ) { + $product->set_tax_status( wc_clean( $data['tax_status'] ) ); + } + + // Tax Class + if ( isset( $data['tax_class'] ) ) { + $product->set_tax_class( wc_clean( $data['tax_class'] ) ); + } + + // Catalog Visibility + if ( isset( $data['catalog_visibility'] ) ) { + $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); + } + + // Purchase Note + if ( isset( $data['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); + } + + // Featured Product + if ( isset( $data['featured'] ) ) { + $product->set_featured( $data['featured'] ); + } + + // Shipping data + $product = $this->save_product_shipping_data( $product, $data ); + + // SKU + if ( isset( $data['sku'] ) ) { + $sku = $product->get_sku(); + $new_sku = wc_clean( $data['sku'] ); + + if ( '' == $new_sku ) { + $product->set_sku( '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); + if ( ! $unique_sku ) { + throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + $product->set_sku( $new_sku ); + } + } else { + $product->set_sku( '' ); + } + } + } + + // Attributes + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + + foreach ( $data['attributes'] as $attribute ) { + $is_taxonomy = 0; + $taxonomy = 0; + + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $attribute_slug = sanitize_title( $attribute['name'] ); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + $attribute_slug = sanitize_title( $attribute['slug'] ); + } + + if ( $taxonomy ) { + $is_taxonomy = 1; + } + + if ( $is_taxonomy ) { + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + // Update post terms + if ( taxonomy_exists( $taxonomy ) ) { + wp_set_object_terms( $product->get_id(), $values, $taxonomy ); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $taxonomy ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Array based + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + + // Text based, separate by pipe + } else { + $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute['name'] ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + + uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); + + $product->set_attributes( $attributes ); + } + + // Sales and prices + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + + } else { + + // Regular Price + if ( isset( $data['regular_price'] ) ) { + $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; + } else { + $regular_price = $product->get_regular_price(); + } + + // Sale Price + if ( isset( $data['sale_price'] ) ) { + $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; + } else { + $sale_price = $product->get_sale_price(); + } + + $product->set_regular_price( $regular_price ); + $product->set_sale_price( $sale_price ); + + if ( isset( $data['sale_price_dates_from'] ) ) { + $date_from = $data['sale_price_dates_from']; + } else { + $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $date_to = $data['sale_price_dates_to']; + } else { + $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; + } + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); + } + + $product->set_date_on_sale_to( $date_to ); + $product->set_date_on_sale_from( $date_from ); + if ( $product->is_on_sale() ) { + $product->set_price( $product->get_sale_price() ); + } else { + $product->set_price( $product->get_regular_price() ); + } + } + + // Product parent ID for groups + if ( isset( $data['parent_id'] ) ) { + $product->set_parent_id( absint( $data['parent_id'] ) ); + } + + // Sold Individually + if ( isset( $data['sold_individually'] ) ) { + $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); + } + + // Stock status + if ( isset( $data['in_stock'] ) ) { + $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock Data + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock + if ( isset( $data['managing_stock'] ) ) { + $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; + $product->set_manage_stock( $managing_stock ); + } else { + $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; + } + + // Backorders + if ( isset( $data['backorders'] ) ) { + if ( 'notify' == $data['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; + } + + $product->set_backorders( $backorders ); + } else { + $backorders = $product->get_backorders(); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( 'yes' == $managing_stock ) { + $product->set_backorders( $backorders ); + + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity + if ( isset( $data['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_backorders( $backorders ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells + if ( isset( $data['upsell_ids'] ) ) { + $upsells = array(); + $ids = $data['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + + $product->set_upsell_ids( $upsells ); + } else { + $product->set_upsell_ids( array() ); + } + } + + // Cross sells + if ( isset( $data['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $data['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + + $product->set_cross_sell_ids( $crosssells ); + } else { + $product->set_cross_sell_ids( array() ); + } + } + + // Product categories + if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { + $product->set_category_ids( $data['categories'] ); + } + + // Product tags + if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { + $product->set_tag_ids( $data['tags'] ); + } + + // Downloadable + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; + $product->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; + } + + // Downloadable options + if ( 'yes' == $is_downloadable ) { + + // Downloadable files + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $data['downloads'] ); + } + + // Download limit + if ( isset( $data['download_limit'] ) ) { + $product->set_download_limit( $data['download_limit'] ); + } + + // Download expiry + if ( isset( $data['download_expiry'] ) ) { + $product->set_download_expiry( $data['download_expiry'] ); + } + } + + // Product url + if ( $product->is_type( 'external' ) ) { + if ( isset( $data['product_url'] ) ) { + $product->set_product_url( $data['product_url'] ); + } + + if ( isset( $data['button_text'] ) ) { + $product->set_button_text( $data['button_text'] ); + } + } + + // Reviews allowed + if ( isset( $data['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $data['reviews_allowed'] ); + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $data ); + } + + // Do action for product type + do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); + + return $product; + } + + /** + * Save variations + * + * @since 2.2 + * @param WC_Product $product + * @param array $request + * + * @return true + * + * @throws WC_API_Exception + */ + protected function save_variations( $product, $request ) { + global $wpdb; + + $id = $product->get_id(); + $attributes = $product->get_attributes(); + + foreach ( $request['variations'] as $menu_order => $data ) { + $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; + $variation = new WC_Product_Variation( $variation_id ); + + // Create initial name and status. + if ( ! $variation->get_slug() ) { + /* translators: 1: variation id 2: product name */ + $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); + $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); + } + + // Parent ID. + $variation->set_parent_id( $product->get_id() ); + + // Menu order. + $variation->set_menu_order( $menu_order ); + + // Status. + if ( isset( $data['visible'] ) ) { + $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { + $image = current( $data['image'] ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->save_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = $data['downloadable']; + $variation->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $variation->get_downloadable(); + } + + // Downloads. + if ( $is_downloadable ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + $manage_stock = (bool) $variation->get_manage_stock(); + if ( isset( $data['managing_stock'] ) ) { + $manage_stock = $data['managing_stock']; + } + $variation->set_manage_stock( $manage_stock ); + + $stock_status = $variation->get_stock_status(); + if ( isset( $data['in_stock'] ) ) { + $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; + } + $variation->set_stock_status( $stock_status ); + + $backorders = $variation->get_backorders(); + if ( isset( $data['backorders'] ) ) { + $backorders = $data['backorders']; + } + $variation->set_backorders( $backorders ); + + if ( $manage_stock ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $_attributes = array(); + + foreach ( $data['attributes'] as $attribute_key => $attribute ) { + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $taxonomy = 0; + $_attribute = array(); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + } + + if ( ! $taxonomy ) { + $taxonomy = sanitize_title( $attribute['name'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + } + + if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = sanitize_title( $_attribute['name'] ); + + if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; + } else { + $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + } + + $_attributes[ $_attribute_key ] = $_attribute_value; + } + } + + $variation->set_attributes( $_attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); + } + + return true; + } + + /** + * Save product shipping data + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + */ + private function save_product_shipping_data( $product, $data ) { + if ( isset( $data['weight'] ) ) { + $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Product dimensions + if ( isset( $data['dimensions'] ) ) { + // Height + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); + } + + // Width + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); + } + + // Length + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); + } + } + + // Virtual + if ( isset( $data['virtual'] ) ) { + $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; + + if ( 'yes' == $virtual ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } + } + + // Shipping class + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + if ( $shipping_class_id ) { + $product->set_shipping_class_id( $shipping_class_id ); + } + } + + return $product; + } + + /** + * Save downloadable files + * + * @since 2.2 + * @param WC_Product $product + * @param array $downloads + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Get attribute taxonomy by slug. + * + * @since 2.2 + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * Get the images for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_images( $product ) { + $images = $attachment_ids = array(); + $product_image = $product->get_image_id(); + + // Add featured image. + if ( ! empty( $product_image ) ) { + $attachment_ids[] = $product_image; + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + + $attachment_post = get_post( $attachment_id ); + + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'title' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + + $images[] = array( + 'id' => 0, + 'created_at' => $this->server->format_datetime( time() ), // Default to now. + 'updated_at' => $this->server->format_datetime( time() ), + 'src' => wc_placeholder_img_src(), + 'title' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Save product images + * + * @since 2.2 + * + * @param WC_Product $product + * @param array $images + * + * @return WC_Product + * @throws WC_API_Exception + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + if ( isset( $image['position'] ) && 0 == $image['position'] ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $product->set_image_id( $attachment_id ); + } else { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $gallery[] = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } else { + $gallery[] = $attachment_id; + } + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Upload image from URL + * + * @since 2.2 + * + * @param string $image_url + * + * @return array|WP_Error + * + * @throws WC_API_Exception + */ + public function upload_product_image( $image_url ) { + $file_name = basename( current( explode( '?', $image_url ) ) ); + $parsed_url = @parse_url( $image_url ); + + // Check parsed URL + if ( ! $parsed_url || ! is_array( $parsed_url ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_image', sprintf( __( 'Invalid URL %s.', 'woocommerce' ), $image_url ), 400 ); + } + + // Ensure url is valid + $image_url = str_replace( ' ', '%20', $image_url ); + + // Get the file + $response = wp_safe_remote_get( $image_url, array( + 'timeout' => 10, + ) ); + + if ( is_wp_error( $response ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_remote_product_image', sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' . sprintf( __( 'Error: %s.', 'woocommerce' ), $response->get_error_message() ), 400 ); + } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_remote_product_image', sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ), 400 ); + } + + // Ensure we have a file name and type + $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() ); + + if ( ! $wp_filetype['type'] ) { + $headers = wp_remote_retrieve_headers( $response ); + if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) { + $disposition = end( explode( 'filename=', $headers['content-disposition'] ) ); + $disposition = sanitize_file_name( $disposition ); + $file_name = $disposition; + } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) { + $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] ); + } + unset( $headers ); + + // Recheck filetype + $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() ); + + if ( ! $wp_filetype['type'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_image', __( 'Invalid image type.', 'woocommerce' ), 400 ); + } + } + + // Upload the file + $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) ); + + if ( $upload['error'] ) { + throw new WC_API_Exception( 'woocommerce_api_product_image_upload_error', $upload['error'], 400 ); + } + + // Get filesize + $filesize = filesize( $upload['file'] ); + + if ( 0 == $filesize ) { + @unlink( $upload['file'] ); + unset( $upload ); + throw new WC_API_Exception( 'woocommerce_api_product_image_upload_file_error', __( 'Zero size file downloaded.', 'woocommerce' ), 400 ); + } + + unset( $response ); + + return $upload; + } + + /** + * Sets product image as attachment and returns the attachment ID. + * + * @since 2.2 + * @param array $upload + * @param int $id + * @return int + */ + protected function set_product_image_as_attachment( $upload, $id ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = wc_clean( $image_meta['title'] ); + } + if ( trim( $image_meta['caption'] ) ) { + $content = wc_clean( $image_meta['caption'] ); + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title, + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; + } + + /** + * Get attribute options. + * + * @param int $product_id + * @param array $attribute + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_attributes( $product ) { + + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + + // variation attributes + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + + // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` + $attributes[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ), + 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => wc_attribute_label( $attribute['name'] ), + 'slug' => str_replace( 'pa_', '', $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get the downloads for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_downloads( $product ) { + + $downloads = array(); + + if ( $product->is_downloadable() ) { + + foreach ( $product->get_downloads() as $file_id => $file ) { + + $downloads[] = array( + 'id' => $file_id, // do not cast as int as this is a hash + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get a listing of product attributes + * + * @since 2.4.0 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attributes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $product_attributes = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $attribute ) { + $product_attributes[] = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + } + + return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute for the given ID + * + * @since 2.4.0 + * + * @param string $id product attribute term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attribute( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $product_attribute = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + + return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Validate attribute data. + * + * @since 2.4.0 + * @param string $name + * @param string $slug + * @param string $type + * @param string $order_by + * @param bool $new_data + * @return bool + * @throws WC_API_Exception + */ + protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { + if ( empty( $name ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + if ( strlen( $slug ) >= 28 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } + + // Validate the attribute type + if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); + } + + // Validate the attribute order by + if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); + } + + return true; + } + + /** + * Create a new product attribute + * + * @since 2.4.0 + * + * @param array $data posted data + * + * @return array|WP_Error + */ + public function create_product_attribute( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $data = $data['product_attribute']; + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); + + if ( ! isset( $data['name'] ) ) { + $data['name'] = ''; + } + + // Set the attribute slug + if ( ! isset( $data['slug'] ) ) { + $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); + } else { + $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); + } + + // Set attribute type when not sent + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'select'; + } + + // Set order by when not sent + if ( ! isset( $data['order_by'] ) ) { + $data['order_by'] = 'menu_order'; + } + + // Validate the attribute data + $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $data['name'], + 'attribute_name' => $data['slug'], + 'attribute_type' => $data['type'], + 'attribute_orderby' => $data['order_by'], + 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, + ), + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for an error in the product creation + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); + } + + $id = $wpdb->insert_id; + + do_action( 'woocommerce_api_create_product_attribute', $id, $data ); + + // Clear transients + delete_transient( 'wc_attribute_taxonomies' ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute + * + * @since 2.4.0 + * + * @param int $id the attribute ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute']; + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); + $attribute = $this->get_product_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; + $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; + $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; + + if ( isset( $data['slug'] ) ) { + $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); + } else { + $attribute_slug = $attribute['product_attribute']['slug']; + } + $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); + + if ( isset( $data['has_archives'] ) ) { + $attribute_public = true === $data['has_archives'] ? 1 : 0; + } else { + $attribute_public = $attribute['product_attribute']['has_archives']; + } + + // Validate the attribute data + $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $attribute_name, + 'attribute_name' => $attribute_slug, + 'attribute_type' => $attribute_type, + 'attribute_orderby' => $attribute_order_by, + 'attribute_public' => $attribute_public, + ), + array( 'attribute_id' => $id ), + array( '%s', '%s', '%s', '%s', '%d' ), + array( '%d' ) + ); + + // Checks for an error in the product creation + if ( false === $update ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); + + // Clear transients + delete_transient( 'wc_attribute_taxonomies' ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute + * + * @since 2.4.0 + * + * @param int $id the product attribute ID + * + * @return array|WP_Error + */ + public function delete_product_attribute( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + $attribute_name = $wpdb->get_var( $wpdb->prepare( " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_null( $attribute_name ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); + do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); + + // Clear transients + delete_transient( 'wc_attribute_taxonomies' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get product by SKU + * + * @deprecated 2.4.0 + * + * @since 2.3.0 + * + * @param int $sku the product SKU + * @param string $fields + * + * @return array|WP_Error + */ + public function get_product_by_sku( $sku, $fields = null ) { + try { + $id = wc_get_product_id_by_sku( $sku ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_sku', __( 'Invalid product SKU', 'woocommerce' ), 404 ); + } + + return $this->get_product( $id, $fields ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear product + * + * @param int $product_id + */ + protected function clear_product( $product_id ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + // Delete product attachments + $attachments = get_children( array( + 'post_parent' => $product_id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product + $product = wc_get_product( $product_id ); + $product->delete(); + } + + /** + * Bulk update or insert products + * Accepts an array with products in the formats supported by + * WC_API_Products->create_product() and WC_API_Products->edit_product() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['products'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); + } + + $data = $data['products']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $products = array(); + + foreach ( $data as $_product ) { + $product_id = 0; + $product_sku = ''; + + // Try to get the product ID + if ( isset( $_product['id'] ) ) { + $product_id = intval( $_product['id'] ); + } + + if ( ! $product_id && isset( $_product['sku'] ) ) { + $product_sku = wc_clean( $_product['sku'] ); + $product_id = wc_get_product_id_by_sku( $product_sku ); + } + + if ( $product_id ) { + + // Product exists / edit product + $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); + + if ( is_wp_error( $edit ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $products[] = $edit['product']; + } + } else { + + // Product don't exists / create product + $new = $this->create_product( array( 'product' => $_product ) ); + + if ( is_wp_error( $new ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $products[] = $new['product']; + } + } + } + + return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v2/class-wc-api-reports.php b/includes/api/legacy/v2/class-wc-api-reports.php new file mode 100644 index 00000000000..8387a2e7b9b --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-reports.php @@ -0,0 +1,329 @@ +base ] = array( + array( array( $this, 'get_reports' ), WC_API_Server::READABLE ), + ); + + # GET /reports/sales + $routes[ $this->base . '/sales' ] = array( + array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ), + ); + + # GET /reports/sales/top_sellers + $routes[ $this->base . '/sales/top_sellers' ] = array( + array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ), + ); + + return $routes; + } + + /** + * Get a simple listing of available reports + * + * @since 2.1 + * @return array + */ + public function get_reports() { + return array( 'reports' => array( 'sales', 'sales/top_sellers' ) ); + } + + /** + * Get the sales report + * + * @since 2.1 + * @param string $fields fields to include in response + * @param array $filter date filtering + * @return array|WP_Error + */ + public function get_sales_report( $fields = null, $filter = array() ) { + + // check user permissions + $check = $this->validate_request(); + + // check for WP_Error + if ( is_wp_error( $check ) ) { + return $check; + } + + // set date filtering + $this->setup_report( $filter ); + + // new customers + $users_query = new WP_User_Query( + array( + 'fields' => array( 'user_registered' ), + 'role' => 'customer', + ) + ); + + $customers = $users_query->get_results(); + + foreach ( $customers as $key => $customer ) { + if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { + unset( $customers[ $key ] ); + } + } + + $total_customers = count( $customers ); + $report_data = $this->report->get_report_data(); + $period_totals = array(); + + // setup period totals by ensuring each period in the interval has data + for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { + + switch ( $this->report->chart_groupby ) { + case 'day' : + $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); + break; + default : + $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); + break; + } + + // set the customer signups for each period + $customer_count = 0; + foreach ( $customers as $customer ) { + if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { + $customer_count++; + } + } + + $period_totals[ $time ] = array( + 'sales' => wc_format_decimal( 0.00, 2 ), + 'orders' => 0, + 'items' => 0, + 'tax' => wc_format_decimal( 0.00, 2 ), + 'shipping' => wc_format_decimal( 0.00, 2 ), + 'discount' => wc_format_decimal( 0.00, 2 ), + 'customers' => $customer_count, + ); + } + + // add total sales, total order count, total tax and total shipping for each period + foreach ( $report_data->orders as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); + $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); + $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); + } + + foreach ( $report_data->order_counts as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['orders'] = (int) $order->count; + } + + // add total order items for each period + foreach ( $report_data->order_items as $order_item ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; + } + + // add total discount for each period + foreach ( $report_data->coupons as $discount ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); + } + + $sales_data = array( + 'total_sales' => $report_data->total_sales, + 'net_sales' => $report_data->net_sales, + 'average_sales' => $report_data->average_sales, + 'total_orders' => $report_data->total_orders, + 'total_items' => $report_data->total_items, + 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), + 'total_shipping' => $report_data->total_shipping, + 'total_refunds' => $report_data->total_refunds, + 'total_discount' => $report_data->total_coupons, + 'totals_grouped_by' => $this->report->chart_groupby, + 'totals' => $period_totals, + 'total_customers' => $total_customers, + ); + + return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) ); + } + + /** + * Get the top sellers report + * + * @since 2.1 + * @param string $fields fields to include in response + * @param array $filter date filtering + * @return array|WP_Error + */ + public function get_top_sellers_report( $fields = null, $filter = array() ) { + + // check user permissions + $check = $this->validate_request(); + + if ( is_wp_error( $check ) ) { + return $check; + } + + // set date filtering + $this->setup_report( $filter ); + + $top_sellers = $this->report->get_order_report_data( array( + 'data' => array( + '_product_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => '', + 'name' => 'product_id', + ), + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ), + ), + 'order_by' => 'order_item_qty DESC', + 'group_by' => 'product_id', + 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + $top_sellers_data = array(); + + foreach ( $top_sellers as $top_seller ) { + + $product = wc_get_product( $top_seller->product_id ); + + if ( $product ) { + $top_sellers_data[] = array( + 'title' => $product->get_name(), + 'product_id' => $top_seller->product_id, + 'quantity' => $top_seller->order_item_qty, + ); + } + } + + return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) ); + } + + /** + * Setup the report object and parse any date filtering + * + * @since 2.1 + * @param array $filter date filtering + */ + private function setup_report( $filter ) { + + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); + + $this->report = new WC_Report_Sales_By_Date(); + + if ( empty( $filter['period'] ) ) { + + // custom date range + $filter['period'] = 'custom'; + + if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { + + // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges + $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] ); + $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null; + + } else { + + // default custom range to today + $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); + } + } else { + + // ensure period is valid + if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { + $filter['period'] = 'week'; + } + + // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods + // allow "week" for period instead of "7day" + if ( 'week' === $filter['period'] ) { + $filter['period'] = '7day'; + } + } + + $this->report->calculate_current_range( $filter['period'] ); + } + + /** + * Verify that the current user has permission to view reports + * + * @since 2.1 + * @see WC_API_Resource::validate_request() + * + * @param null $id unused + * @param null $type unused + * @param null $context unused + * + * @return bool|WP_Error + */ + protected function validate_request( $id = null, $type = null, $context = null ) { + + if ( ! current_user_can( 'view_woocommerce_reports' ) ) { + + return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) ); + + } else { + + return true; + } + } +} diff --git a/includes/api/legacy/v2/class-wc-api-resource.php b/includes/api/legacy/v2/class-wc-api-resource.php new file mode 100644 index 00000000000..89c3feef0fd --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-resource.php @@ -0,0 +1,467 @@ +server = $server; + + // automatically register routes for sub-classes + add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); + + // maybe add meta to top-level resource responses + foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { + add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); + } + + $response_names = array( + 'order', + 'coupon', + 'customer', + 'product', + 'report', + 'customer_orders', + 'customer_downloads', + 'order_note', + 'order_refund', + 'product_reviews', + 'product_category', + ); + + foreach ( $response_names as $name ) { + + /** + * Remove fields from responses when requests specify certain fields + * note these are hooked at a later priority so data added via + * filters (e.g. customer data to the order response) still has the + * fields filtered properly + */ + add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 ); + } + } + + /** + * Validate the request by checking: + * + * 1) the ID is a valid integer + * 2) the ID returns a valid post object and matches the provided post type + * 3) the current user has the proper permissions to read/edit/delete the post + * + * @since 2.1 + * @param string|int $id the post ID + * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` + * @param string $context the context of the request, either `read`, `edit` or `delete` + * @return int|WP_Error valid post ID or WP_Error if any of the checks fails + */ + protected function validate_request( $id, $type, $context ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); + } + + // Only custom post types have per-post type/permission checks + if ( 'customer' !== $type ) { + + $post = get_post( $id ); + + if ( null === $post ) { + return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) ); + } + + // For checking permissions, product variations are the same as the product post type + $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; + + // Validate post type + if ( $type !== $post_type ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); + } + + // Validate permissions + switch ( $context ) { + + case 'read': + if ( ! $this->is_readable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'edit': + if ( ! $this->is_editable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'delete': + if ( ! $this->is_deletable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + } + } + + return $id; + } + + /** + * Add common request arguments to argument list before WP_Query is run + * + * @since 2.1 + * @param array $base_args required arguments for the query (e.g. `post_type`, etc) + * @param array $request_args arguments provided in the request + * @return array + */ + protected function merge_query_args( $base_args, $request_args ) { + + $args = array(); + + // date + if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { + + $args['date_query'] = array(); + + // resources created after specified date + if ( ! empty( $request_args['created_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); + } + + // resources created before specified date + if ( ! empty( $request_args['created_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); + } + + // resources updated after specified date + if ( ! empty( $request_args['updated_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); + } + + // resources updated before specified date + if ( ! empty( $request_args['updated_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); + } + } + + // search + if ( ! empty( $request_args['q'] ) ) { + $args['s'] = $request_args['q']; + } + + // resources per response + if ( ! empty( $request_args['limit'] ) ) { + $args['posts_per_page'] = $request_args['limit']; + } + + // resource offset + if ( ! empty( $request_args['offset'] ) ) { + $args['offset'] = $request_args['offset']; + } + + // order (ASC or DESC, ASC by default) + if ( ! empty( $request_args['order'] ) ) { + $args['order'] = $request_args['order']; + } + + // orderby + if ( ! empty( $request_args['orderby'] ) ) { + $args['orderby'] = $request_args['orderby']; + + // allow sorting by meta value + if ( ! empty( $request_args['orderby_meta_key'] ) ) { + $args['meta_key'] = $request_args['orderby_meta_key']; + } + } + + // allow post status change + if ( ! empty( $request_args['post_status'] ) ) { + $args['post_status'] = $request_args['post_status']; + unset( $request_args['post_status'] ); + } + + // filter by a list of post id + if ( ! empty( $request_args['in'] ) ) { + $args['post__in'] = explode( ',', $request_args['in'] ); + unset( $request_args['in'] ); + } + + // filter by a list of post id + if ( ! empty( $request_args['in'] ) ) { + $args['post__in'] = explode( ',', $request_args['in'] ); + unset( $request_args['in'] ); + } + + // resource page + $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; + + $args = apply_filters( 'woocommerce_api_query_args', $args, $request_args ); + + return array_merge( $base_args, $args ); + } + + /** + * Add meta to resources when requested by the client. Meta is added as a top-level + * `_meta` attribute (e.g. `order_meta`) as a list of key/value pairs + * + * @since 2.1 + * @param array $data the resource data + * @param object $resource the resource object (e.g WC_Order) + * @return mixed + */ + public function maybe_add_meta( $data, $resource ) { + + if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { + + // don't attempt to add meta more than once + if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { + return $data; + } + + // define the top-level property name for the meta + switch ( get_class( $resource ) ) { + + case 'WC_Order': + $meta_name = 'order_meta'; + break; + + case 'WC_Coupon': + $meta_name = 'coupon_meta'; + break; + + case 'WP_User': + $meta_name = 'customer_meta'; + break; + + default: + $meta_name = 'product_meta'; + break; + } + + if ( is_a( $resource, 'WP_User' ) ) { + + // customer meta + $meta = (array) get_user_meta( $resource->ID ); + + } else { + + // coupon/order/product meta + $meta = (array) get_post_meta( $resource->get_id() ); + } + + foreach ( $meta as $meta_key => $meta_value ) { + + // don't add hidden meta by default + if ( ! is_protected_meta( $meta_key ) ) { + $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); + } + } + } + + return $data; + } + + /** + * Restrict the fields included in the response if the request specified certain only certain fields should be returned + * + * @since 2.1 + * @param array $data the response data + * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order + * @param array|string the requested list of fields to include in the response + * @return array response data + */ + public function filter_response_fields( $data, $resource, $fields ) { + + if ( ! is_array( $data ) || empty( $fields ) ) { + return $data; + } + + $fields = explode( ',', $fields ); + $sub_fields = array(); + + // get sub fields + foreach ( $fields as $field ) { + + if ( false !== strpos( $field, '.' ) ) { + + list( $name, $value ) = explode( '.', $field ); + + $sub_fields[ $name ] = $value; + } + } + + // iterate through top-level fields + foreach ( $data as $data_field => $data_value ) { + + // if a field has sub-fields and the top-level field has sub-fields to filter + if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { + + // iterate through each sub-field + foreach ( $data_value as $sub_field => $sub_field_value ) { + + // remove non-matching sub-fields + if ( ! in_array( $sub_field, $sub_fields ) ) { + unset( $data[ $data_field ][ $sub_field ] ); + } + } + } else { + + // remove non-matching top-level fields + if ( ! in_array( $data_field, $fields ) ) { + unset( $data[ $data_field ] ); + } + } + } + + return $data; + } + + /** + * Delete a given resource + * + * @since 2.1 + * @param int $id the resource ID + * @param string $type the resource post type, or `customer` + * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) + * @return array|WP_Error + */ + protected function delete( $id, $type, $force = false ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + if ( 'customer' === $type ) { + + $result = wp_delete_user( $id ); + + if ( $result ) { + return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); + } else { + return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); + } + } else { + + // delete order/coupon/webhook + $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); + + if ( ! $result ) { + return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); + } else { + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); + } + } + } + + + /** + * Checks if the given post is readable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_readable( $post ) { + + return $this->check_permission( $post, 'read' ); + } + + /** + * Checks if the given post is editable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_editable( $post ) { + + return $this->check_permission( $post, 'edit' ); + + } + + /** + * Checks if the given post is deletable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_deletable( $post ) { + + return $this->check_permission( $post, 'delete' ); + } + + /** + * Checks the permissions for the current user given a post and context + * + * @since 2.1 + * @param WP_Post|int $post + * @param string $context the type of permission to check, either `read`, `write`, or `delete` + * @return bool true if the current user has the permissions to perform the context on the post + */ + private function check_permission( $post, $context ) { + + if ( ! is_a( $post, 'WP_Post' ) ) { + $post = get_post( $post ); + } + + if ( is_null( $post ) ) { + return false; + } + + $post_type = get_post_type_object( $post->post_type ); + + if ( 'read' === $context ) { + return ( 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ) ); + } elseif ( 'edit' === $context ) { + return current_user_can( $post_type->cap->edit_post, $post->ID ); + } elseif ( 'delete' === $context ) { + return current_user_can( $post_type->cap->delete_post, $post->ID ); + } else { + return false; + } + } +} diff --git a/includes/api/legacy/v2/class-wc-api-server.php b/includes/api/legacy/v2/class-wc-api-server.php new file mode 100644 index 00000000000..e6413dca5ce --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-server.php @@ -0,0 +1,769 @@ + self::METHOD_GET, + 'GET' => self::METHOD_GET, + 'POST' => self::METHOD_POST, + 'PUT' => self::METHOD_PUT, + 'PATCH' => self::METHOD_PATCH, + 'DELETE' => self::METHOD_DELETE, + ); + + /** + * Requested path (relative to the API root, wp-json.php) + * + * @var string + */ + public $path = ''; + + /** + * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE) + * + * @var string + */ + public $method = 'HEAD'; + + /** + * Request parameters + * + * This acts as an abstraction of the superglobals + * (GET => $_GET, POST => $_POST) + * + * @var array + */ + public $params = array( 'GET' => array(), 'POST' => array() ); + + /** + * Request headers + * + * @var array + */ + public $headers = array(); + + /** + * Request files (matches $_FILES) + * + * @var array + */ + public $files = array(); + + /** + * Request/Response handler, either JSON by default + * or XML if requested by client + * + * @var WC_API_Handler + */ + public $handler; + + + /** + * Setup class and set request/response handler + * + * @since 2.1 + * @param $path + * @return WC_API_Server + */ + public function __construct( $path ) { + + if ( empty( $path ) ) { + if ( isset( $_SERVER['PATH_INFO'] ) ) { + $path = $_SERVER['PATH_INFO']; + } else { + $path = '/'; + } + } + + $this->path = $path; + $this->method = $_SERVER['REQUEST_METHOD']; + $this->params['GET'] = $_GET; + $this->params['POST'] = $_POST; + $this->headers = $this->get_headers( $_SERVER ); + $this->files = $_FILES; + + // Compatibility for clients that can't use PUT/PATCH/DELETE + if ( isset( $_GET['_method'] ) ) { + $this->method = strtoupper( $_GET['_method'] ); + } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { + $this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; + } + + // load response handler + $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this ); + + $this->handler = new $handler_class(); + } + + /** + * Check authentication for the request + * + * @since 2.1 + * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login + */ + public function check_authentication() { + + // allow plugins to remove default authentication or add their own authentication + $user = apply_filters( 'woocommerce_api_check_authentication', null, $this ); + + if ( is_a( $user, 'WP_User' ) ) { + + // API requests run under the context of the authenticated user + wp_set_current_user( $user->ID ); + + } elseif ( ! is_wp_error( $user ) ) { + + // WP_Errors are handled in serve_request() + $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) ); + + } + + return $user; + } + + /** + * Convert an error to an array + * + * This iterates over all error codes and messages to change it into a flat + * array. This enables simpler client behaviour, as it is represented as a + * list in JSON rather than an object/map + * + * @since 2.1 + * @param WP_Error $error + * @return array List of associative arrays with code and message keys + */ + protected function error_to_array( $error ) { + $errors = array(); + foreach ( (array) $error->errors as $code => $messages ) { + foreach ( (array) $messages as $message ) { + $errors[] = array( 'code' => $code, 'message' => $message ); + } + } + + return array( 'errors' => $errors ); + } + + /** + * Handle serving an API request + * + * Matches the current server URI to a route and runs the first matching + * callback then outputs a JSON representation of the returned value. + * + * @since 2.1 + * @uses WC_API_Server::dispatch() + */ + public function serve_request() { + + do_action( 'woocommerce_api_server_before_serve', $this ); + + $this->header( 'Content-Type', $this->handler->get_content_type(), true ); + + // the API is enabled by default + if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) { + + $this->send_status( 404 ); + + echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) ); + + return; + } + + $result = $this->check_authentication(); + + // if authorization check was successful, dispatch the request + if ( ! is_wp_error( $result ) ) { + $result = $this->dispatch(); + } + + // handle any dispatch errors + if ( is_wp_error( $result ) ) { + $data = $result->get_error_data(); + if ( is_array( $data ) && isset( $data['status'] ) ) { + $this->send_status( $data['status'] ); + } + + $result = $this->error_to_array( $result ); + } + + // This is a filter rather than an action, since this is designed to be + // re-entrant if needed + $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this ); + + if ( ! $served ) { + + if ( 'HEAD' === $this->method ) { + return; + } + + echo $this->handler->generate_response( $result ); + } + } + + /** + * Retrieve the route map + * + * The route map is an associative array with path regexes as the keys. The + * value is an indexed array with the callback function/method as the first + * item, and a bitmask of HTTP methods as the second item (see the class + * constants). + * + * Each route can be mapped to more than one callback by using an array of + * the indexed arrays. This allows mapping e.g. GET requests to one callback + * and POST requests to another. + * + * Note that the path regexes (array keys) must have @ escaped, as this is + * used as the delimiter with preg_match() + * + * @since 2.1 + * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` + */ + public function get_routes() { + + // index added by default + $endpoints = array( + + '/' => array( array( $this, 'get_index' ), self::READABLE ), + ); + + $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints ); + + // Normalise the endpoints + foreach ( $endpoints as $route => &$handlers ) { + if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) { + $handlers = array( $handlers ); + } + } + + return $endpoints; + } + + /** + * Match the request to a callback and call it + * + * @since 2.1 + * @return mixed The value returned by the callback, or a WP_Error instance + */ + public function dispatch() { + + switch ( $this->method ) { + + case 'HEAD' : + case 'GET' : + $method = self::METHOD_GET; + break; + + case 'POST' : + $method = self::METHOD_POST; + break; + + case 'PUT' : + $method = self::METHOD_PUT; + break; + + case 'PATCH' : + $method = self::METHOD_PATCH; + break; + + case 'DELETE' : + $method = self::METHOD_DELETE; + break; + + default : + return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) ); + } + + foreach ( $this->get_routes() as $route => $handlers ) { + foreach ( $handlers as $handler ) { + $callback = $handler[0]; + $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; + + if ( ! ( $supported & $method ) ) { + continue; + } + + $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args ); + + if ( ! $match ) { + continue; + } + + if ( ! is_callable( $callback ) ) { + return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $args = array_merge( $args, $this->params['GET'] ); + if ( $method & self::METHOD_POST ) { + $args = array_merge( $args, $this->params['POST'] ); + } + if ( $supported & self::ACCEPT_DATA ) { + $data = $this->handler->parse_body( $this->get_raw_data() ); + $args = array_merge( $args, array( 'data' => $data ) ); + } elseif ( $supported & self::ACCEPT_RAW_DATA ) { + $data = $this->get_raw_data(); + $args = array_merge( $args, array( 'data' => $data ) ); + } + + $args['_method'] = $method; + $args['_route'] = $route; + $args['_path'] = $this->path; + $args['_headers'] = $this->headers; + $args['_files'] = $this->files; + + $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback ); + + // Allow plugins to halt the request via this filter + if ( is_wp_error( $args ) ) { + return $args; + } + + $params = $this->sort_callback_params( $callback, $args ); + if ( is_wp_error( $params ) ) { + return $params; + } + + return call_user_func_array( $callback, $params ); + } + } + + return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) ); + } + + /** + * urldecode deep. + * + * @since 2.2 + * @param string|array $value Data to decode with urldecode. + * @return string|array Decoded data. + */ + protected function urldecode_deep( $value ) { + if ( is_array( $value ) ) { + return array_map( array( $this, 'urldecode_deep' ), $value ); + } else { + return urldecode( $value ); + } + } + + /** + * Sort parameters by order specified in method declaration + * + * Takes a callback and a list of available params, then filters and sorts + * by the parameters the method actually needs, using the Reflection API + * + * @since 2.2 + * + * @param callable|array $callback the endpoint callback + * @param array $provided the provided request parameters + * + * @return array|WP_Error + */ + protected function sort_callback_params( $callback, $provided ) { + if ( is_array( $callback ) ) { + $ref_func = new ReflectionMethod( $callback[0], $callback[1] ); + } else { + $ref_func = new ReflectionFunction( $callback ); + } + + $wanted = $ref_func->getParameters(); + $ordered_parameters = array(); + + foreach ( $wanted as $param ) { + if ( isset( $provided[ $param->getName() ] ) ) { + // We have this parameters in the list to choose from + if ( 'data' == $param->getName() ) { + $ordered_parameters[] = $provided[ $param->getName() ]; + continue; + } + + $ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] ); + } elseif ( $param->isDefaultValueAvailable() ) { + // We don't have this parameter, but it's optional + $ordered_parameters[] = $param->getDefaultValue(); + } else { + // We don't have this parameter and it wasn't optional, abort! + return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) ); + } + } + + return $ordered_parameters; + } + + /** + * Get the site index. + * + * This endpoint describes the capabilities of the site. + * + * @since 2.3 + * @return array Index entity + */ + public function get_index() { + + // General site data + $available = array( + 'store' => array( + 'name' => get_option( 'blogname' ), + 'description' => get_option( 'blogdescription' ), + 'URL' => get_option( 'siteurl' ), + 'wc_version' => WC()->version, + 'routes' => array(), + 'meta' => array( + 'timezone' => wc_timezone_string(), + 'currency' => get_woocommerce_currency(), + 'currency_format' => get_woocommerce_currency_symbol(), + 'currency_position' => get_option( 'woocommerce_currency_pos' ), + 'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ), + 'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ), + 'price_num_decimals' => wc_get_price_decimals(), + 'tax_included' => wc_prices_include_tax(), + 'weight_unit' => get_option( 'woocommerce_weight_unit' ), + 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), + 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ), + 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ), + 'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ), + 'links' => array( + 'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/', + ), + ), + ), + ); + + // Find the available routes + foreach ( $this->get_routes() as $route => $callbacks ) { + $data = array(); + + $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); + + foreach ( self::$method_map as $name => $bitmask ) { + foreach ( $callbacks as $callback ) { + // Skip to the next route if any callback is hidden + if ( $callback[1] & self::HIDDEN_ENDPOINT ) { + continue 3; + } + + if ( $callback[1] & $bitmask ) { + $data['supports'][] = $name; + } + + if ( $callback[1] & self::ACCEPT_DATA ) { + $data['accepts_data'] = true; + } + + // For non-variable routes, generate links + if ( strpos( $route, '<' ) === false ) { + $data['meta'] = array( + 'self' => get_woocommerce_api_url( $route ), + ); + } + } + } + + $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data ); + } + + return apply_filters( 'woocommerce_api_index', $available ); + } + + /** + * Send a HTTP status code + * + * @since 2.1 + * @param int $code HTTP status + */ + public function send_status( $code ) { + status_header( $code ); + } + + /** + * Send a HTTP header + * + * @since 2.1 + * @param string $key Header key + * @param string $value Header value + * @param boolean $replace Should we replace the existing header? + */ + public function header( $key, $value, $replace = true ) { + header( sprintf( '%s: %s', $key, $value ), $replace ); + } + + /** + * Send a Link header + * + * @internal The $rel parameter is first, as this looks nicer when sending multiple + * + * @link http://tools.ietf.org/html/rfc5988 + * @link http://www.iana.org/assignments/link-relations/link-relations.xml + * + * @since 2.1 + * @param string $rel Link relation. Either a registered type, or an absolute URL + * @param string $link Target IRI for the link + * @param array $other Other parameters to send, as an associative array + */ + public function link_header( $rel, $link, $other = array() ) { + + $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) ); + + foreach ( $other as $key => $value ) { + + if ( 'title' == $key ) { + + $value = '"' . $value . '"'; + } + + $header .= '; ' . $key . '=' . $value; + } + + $this->header( 'Link', $header, false ); + } + + /** + * Send pagination headers for resources + * + * @since 2.1 + * @param WP_Query|WP_User_Query $query + */ + public function add_pagination_headers( $query ) { + + // WP_User_Query + if ( is_a( $query, 'WP_User_Query' ) ) { + + $single = count( $query->get_results() ) == 1; + $total = $query->get_total(); + + if ( $query->get( 'number' ) > 0 ) { + $page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1; + $total_pages = ceil( $total / $query->get( 'number' ) ); + } else { + $page = 1; + $total_pages = 1; + } + + // WP_Query + } else { + + $page = $query->get( 'paged' ); + $single = $query->is_single(); + $total = $query->found_posts; + $total_pages = $query->max_num_pages; + } + + if ( ! $page ) { + $page = 1; + } + + $next_page = absint( $page ) + 1; + + if ( ! $single ) { + + // first/prev + if ( $page > 1 ) { + $this->link_header( 'first', $this->get_paginated_url( 1 ) ); + $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) ); + } + + // next + if ( $next_page <= $total_pages ) { + $this->link_header( 'next', $this->get_paginated_url( $next_page ) ); + } + + // last + if ( $page != $total_pages ) { + $this->link_header( 'last', $this->get_paginated_url( $total_pages ) ); + } + } + + $this->header( 'X-WC-Total', $total ); + $this->header( 'X-WC-TotalPages', $total_pages ); + + do_action( 'woocommerce_api_pagination_headers', $this, $query ); + } + + /** + * Returns the request URL with the page query parameter set to the specified page + * + * @since 2.1 + * @param int $page + * @return string + */ + private function get_paginated_url( $page ) { + + // remove existing page query param + $request = remove_query_arg( 'page' ); + + // add provided page query param + $request = urldecode( add_query_arg( 'page', $page, $request ) ); + + // get the home host + $host = parse_url( get_home_url(), PHP_URL_HOST ); + + return set_url_scheme( "http://{$host}{$request}" ); + } + + /** + * Retrieve the raw request entity (body) + * + * @since 2.1 + * @return string + */ + public function get_raw_data() { + // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6 + if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { + return file_get_contents( 'php://input' ); + } + + global $HTTP_RAW_POST_DATA; + + // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, + // but we can do it ourself. + if ( ! isset( $HTTP_RAW_POST_DATA ) ) { + $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); + } + + return $HTTP_RAW_POST_DATA; + } + + /** + * Parse an RFC3339 datetime into a MySQl datetime + * + * Invalid dates default to unix epoch + * + * @since 2.1 + * @param string $datetime RFC3339 datetime + * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) + */ + public function parse_datetime( $datetime ) { + + // Strip millisecond precision (a full stop followed by one or more digits) + if ( strpos( $datetime, '.' ) !== false ) { + $datetime = preg_replace( '/\.\d+/', '', $datetime ); + } + + // default timezone to UTC + $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); + + try { + + $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); + + } catch ( Exception $e ) { + + $datetime = new DateTime( '@0' ); + + } + + return $datetime->format( 'Y-m-d H:i:s' ); + } + + /** + * Format a unix timestamp or MySQL datetime into an RFC3339 datetime + * + * @since 2.1 + * @param int|string $timestamp unix timestamp or MySQL datetime + * @param bool $convert_to_utc + * @param bool $convert_to_gmt Use GMT timezone. + * @return string RFC3339 datetime + */ + public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) { + if ( $convert_to_gmt ) { + if ( is_numeric( $timestamp ) ) { + $timestamp = date( 'Y-m-d H:i:s', $timestamp ); + } + + $timestamp = get_gmt_from_date( $timestamp ); + } + + if ( $convert_to_utc ) { + $timezone = new DateTimeZone( wc_timezone_string() ); + } else { + $timezone = new DateTimeZone( 'UTC' ); + } + + try { + + if ( is_numeric( $timestamp ) ) { + $date = new DateTime( "@{$timestamp}" ); + } else { + $date = new DateTime( $timestamp, $timezone ); + } + + // convert to UTC by adjusting the time based on the offset of the site's timezone + if ( $convert_to_utc ) { + $date->modify( -1 * $date->getOffset() . ' seconds' ); + } + } catch ( Exception $e ) { + + $date = new DateTime( '@0' ); + } + + return $date->format( 'Y-m-d\TH:i:s\Z' ); + } + + /** + * Extract headers from a PHP-style $_SERVER array + * + * @since 2.1 + * @param array $server Associative array similar to $_SERVER + * @return array Headers extracted from the input + */ + public function get_headers( $server ) { + $headers = array(); + // CONTENT_* headers are not prefixed with HTTP_ + $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true ); + + foreach ( $server as $key => $value ) { + if ( strpos( $key, 'HTTP_' ) === 0 ) { + $headers[ substr( $key, 5 ) ] = $value; + } elseif ( isset( $additional[ $key ] ) ) { + $headers[ $key ] = $value; + } + } + + return $headers; + } +} diff --git a/includes/api/legacy/v2/class-wc-api-webhooks.php b/includes/api/legacy/v2/class-wc-api-webhooks.php new file mode 100644 index 00000000000..29fbe93f9b9 --- /dev/null +++ b/includes/api/legacy/v2/class-wc-api-webhooks.php @@ -0,0 +1,477 @@ +base ] = array( + array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ), + array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /webhooks/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /webhooks/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ), + ); + + # GET /webhooks//deliveries + $routes[ $this->base . '/(?P\d+)/deliveries' ] = array( + array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), + ); + + # GET /webhooks//deliveries/ + $routes[ $this->base . '/(?P\d+)/deliveries/(?P\d+)' ] = array( + array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ), + ); + + return $routes; + } + + /** + * Get all webhooks + * + * @since 2.2 + * + * @param array $fields + * @param array $filter + * @param string $status + * @param int $page + * + * @return array + */ + public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_webhooks( $filter ); + + $webhooks = array(); + + foreach ( $query->posts as $webhook_id ) { + + if ( ! $this->is_readable( $webhook_id ) ) { + continue; + } + + $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'webhooks' => $webhooks ); + } + + /** + * Get the webhook for the given ID + * + * @since 2.2 + * @param int $id webhook ID + * @param array $fields + * @return array|WP_Error + */ + public function get_webhook( $id, $fields = null ) { + + // ensure webhook ID is valid & user has permission to read + $id = $this->validate_request( $id, 'shop_webhook', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $webhook = new WC_Webhook( $id ); + + $webhook_data = array( + 'id' => $webhook->id, + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'created_at' => $this->server->format_datetime( $webhook->get_post_data()->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $webhook->get_post_data()->post_modified_gmt ), + ); + + return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) ); + } + + /** + * Get the total number of webhooks + * + * @since 2.2 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_webhooks_count( $status = null, $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_shop_webhooks' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $query = $this->query_webhooks( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create an webhook + * + * @since 2.2 + * + * @param array $data parsed webhook data + * + * @return array|WP_Error + */ + public function create_webhook( $data ) { + + try { + if ( ! isset( $data['webhook'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 ); + } + + $data = $data['webhook']; + + // permission check + if ( ! current_user_can( 'publish_shop_webhooks' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this ); + + // validate topic + if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 ); + } + + // validate delivery URL + if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 ); + } + + $webhook_data = apply_filters( 'woocommerce_new_webhook_data', array( + 'post_type' => 'shop_webhook', + 'post_status' => 'publish', + 'ping_status' => 'closed', + 'post_author' => get_current_user_id(), + 'post_password' => strlen( ( $password = uniqid( 'webhook_' ) ) ) > 20 ? substr( $password, 0, 20 ) : $password, + // @codingStandardsIgnoreStart + 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ), + // @codingStandardsIgnoreEnd + ), $data, $this ); + + $webhook_id = wp_insert_post( $webhook_data ); + + if ( is_wp_error( $webhook_id ) || ! $webhook_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_webhook', sprintf( __( 'Cannot create webhook: %s', 'woocommerce' ), is_wp_error( $webhook_id ) ? implode( ', ', $webhook_id->get_error_messages() ) : '0' ), 500 ); + } + + $webhook = new WC_Webhook( $webhook_id ); + + // set topic, delivery URL, and optional secret + $webhook->set_topic( $data['topic'] ); + $webhook->set_delivery_url( $data['delivery_url'] ); + + // set secret if provided, defaults to API users consumer secret + $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : '' ); + + // Set API version to legacy v3. + $webhook->set_api_version( 'legacy_v3' ); + + // send ping + $webhook->deliver_ping(); + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_webhook', $webhook->id, $this ); + + delete_transient( 'woocommerce_webhook_ids' ); + + return $this->get_webhook( $webhook->id ); + + } catch ( WC_API_Exception $e ) { + + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a webhook + * + * @since 2.2 + * + * @param int $id webhook ID + * @param array $data parsed webhook data + * + * @return array|WP_Error + */ + public function edit_webhook( $id, $data ) { + + try { + if ( ! isset( $data['webhook'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 ); + } + + $data = $data['webhook']; + + $id = $this->validate_request( $id, 'shop_webhook', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this ); + + $webhook = new WC_Webhook( $id ); + + // update topic + if ( ! empty( $data['topic'] ) ) { + + if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) { + + $webhook->set_topic( $data['topic'] ); + + } else { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 ); + } + } + + // update delivery URL + if ( ! empty( $data['delivery_url'] ) ) { + if ( wc_is_valid_url( $data['delivery_url'] ) ) { + + $webhook->set_delivery_url( $data['delivery_url'] ); + + } else { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 ); + } + } + + // update secret + if ( ! empty( $data['secret'] ) ) { + $webhook->set_secret( $data['secret'] ); + } + + // update status + if ( ! empty( $data['status'] ) ) { + $webhook->update_status( $data['status'] ); + } + + // update user ID + $webhook_data = array( + 'ID' => $webhook->id, + 'post_author' => get_current_user_id(), + ); + + // update name + if ( ! empty( $data['name'] ) ) { + $webhook_data['post_title'] = $data['name']; + } + + // update post + wp_update_post( $webhook_data ); + + do_action( 'woocommerce_api_edit_webhook', $webhook->id, $this ); + + delete_transient( 'woocommerce_webhook_ids' ); + + return $this->get_webhook( $id ); + + } catch ( WC_API_Exception $e ) { + + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a webhook + * + * @since 2.2 + * @param int $id webhook ID + * @return array|WP_Error + */ + public function delete_webhook( $id ) { + + $id = $this->validate_request( $id, 'shop_webhook', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + do_action( 'woocommerce_api_delete_webhook', $id, $this ); + + delete_transient( 'woocommerce_webhook_ids' ); + + // no way to manage trashed webhooks at the moment, so force delete + return $this->delete( $id, 'webhook', true ); + } + + /** + * Helper method to get webhook post objects + * + * @since 2.2 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_webhooks( $args ) { + + // Set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'shop_webhook', + ); + + // Add status argument + if ( ! empty( $args['status'] ) ) { + + switch ( $args['status'] ) { + case 'active': + $query_args['post_status'] = 'publish'; + break; + case 'paused': + $query_args['post_status'] = 'draft'; + break; + case 'disabled': + $query_args['post_status'] = 'pending'; + break; + default: + $query_args['post_status'] = 'publish'; + } + unset( $args['status'] ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Get deliveries for a webhook + * + * @since 2.2 + * @param string $webhook_id webhook ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_webhook_deliveries( $webhook_id, $fields = null ) { + + // Ensure ID is valid webhook ID + $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' ); + + if ( is_wp_error( $webhook_id ) ) { + return $webhook_id; + } + + $webhook = new WC_Webhook( $webhook_id ); + $logs = $webhook->get_delivery_logs(); + $delivery_logs = array(); + + foreach ( $logs as $log ) { + + // Add timestamp + $log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt ); + + // Remove comment object + unset( $log['comment'] ); + + $delivery_logs[] = $log; + } + + return array( 'webhook_deliveries' => $delivery_logs ); + } + + /** + * Get the delivery log for the given webhook ID and delivery ID + * + * @since 2.2 + * + * @param string $webhook_id webhook ID + * @param string $id delivery log ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_webhook_delivery( $webhook_id, $id, $fields = null ) { + try { + // Validate webhook ID + $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' ); + + if ( is_wp_error( $webhook_id ) ) { + return $webhook_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 ); + } + + $webhook = new WC_Webhook( $webhook_id ); + + $log = $webhook->get_delivery_log( $id ); + + if ( ! $log ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 ); + } + + $delivery_log = $log; + + // Add timestamp + $delivery_log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt ); + + // Remove comment object + unset( $delivery_log['comment'] ); + + return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', $delivery_log, $id, $fields, $log, $webhook_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v2/interface-wc-api-handler.php b/includes/api/legacy/v2/interface-wc-api-handler.php new file mode 100644 index 00000000000..484f9f57f02 --- /dev/null +++ b/includes/api/legacy/v2/interface-wc-api-handler.php @@ -0,0 +1,47 @@ +api->server->path ) { + return new WP_User( 0 ); + } + + try { + if ( is_ssl() ) { + $keys = $this->perform_ssl_authentication(); + } else { + $keys = $this->perform_oauth_authentication(); + } + + // Check API key-specific permission + $this->check_api_key_permissions( $keys['permissions'] ); + + $user = $this->get_user_by_id( $keys['user_id'] ); + + $this->update_api_key_last_access( $keys['key_id'] ); + + } catch ( Exception $e ) { + $user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + return $user; + } + + /** + * SSL-encrypted requests are not subject to sniffing or man-in-the-middle + * attacks, so the request can be authenticated by simply looking up the user + * associated with the given consumer key and confirming the consumer secret + * provided is valid + * + * @since 2.1 + * @return array + * @throws Exception + */ + private function perform_ssl_authentication() { + $params = WC()->api->server->params['GET']; + + // if the $_GET parameters are present, use those first + if ( ! empty( $params['consumer_key'] ) && ! empty( $params['consumer_secret'] ) ) { + $keys = $this->get_keys_by_consumer_key( $params['consumer_key'] ); + + if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $params['consumer_secret'] ) ) { + throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 ); + } + + return $keys; + } + + // if the above is not present, we will do full basic auth + if ( empty( $_SERVER['PHP_AUTH_USER'] ) || empty( $_SERVER['PHP_AUTH_PW'] ) ) { + $this->exit_with_unauthorized_headers(); + } + + $keys = $this->get_keys_by_consumer_key( $_SERVER['PHP_AUTH_USER'] ); + + if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $_SERVER['PHP_AUTH_PW'] ) ) { + $this->exit_with_unauthorized_headers(); + } + + return $keys; + } + + /** + * If the consumer_key and consumer_secret $_GET parameters are NOT provided + * and the Basic auth headers are either not present or the consumer secret does not match the consumer + * key provided, then return the correct Basic headers and an error message. + * + * @since 2.4 + */ + private function exit_with_unauthorized_headers() { + $auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' ); + header( 'WWW-Authenticate: Basic realm="' . $auth_message . '"' ); + header( 'HTTP/1.0 401 Unauthorized' ); + throw new Exception( __( 'Consumer Secret is invalid.', 'woocommerce' ), 401 ); + } + + /** + * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests + * + * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP + * + * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: + * + * 1) There is no token associated with request/responses, only consumer keys/secrets are used + * + * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, + * This is because there is no cross-OS function within PHP to get the raw Authorization header + * + * @link http://tools.ietf.org/html/rfc5849 for the full spec + * @since 2.1 + * @return array + * @throws Exception + */ + private function perform_oauth_authentication() { + + $params = WC()->api->server->params['GET']; + + $param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); + + // Check for required OAuth parameters + foreach ( $param_names as $param_name ) { + + if ( empty( $params[ $param_name ] ) ) { + throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 ); + } + } + + // Fetch WP user by consumer key + $keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] ); + + // Perform OAuth validation + $this->check_oauth_signature( $keys, $params ); + $this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] ); + + // Authentication successful, return user + return $keys; + } + + /** + * Return the keys for the given consumer key + * + * @since 2.4.0 + * @param string $consumer_key + * @return array + * @throws Exception + */ + private function get_keys_by_consumer_key( $consumer_key ) { + global $wpdb; + + $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); + + $keys = $wpdb->get_row( $wpdb->prepare( " + SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces + FROM {$wpdb->prefix}woocommerce_api_keys + WHERE consumer_key = '%s' + ", $consumer_key ), ARRAY_A ); + + if ( empty( $keys ) ) { + throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 ); + } + + return $keys; + } + + /** + * Get user by ID + * + * @since 2.4.0 + * + * @param int $user_id + * + * @return WP_User + * @throws Exception + */ + private function get_user_by_id( $user_id ) { + $user = get_user_by( 'id', $user_id ); + + if ( ! $user ) { + throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 ); + } + + return $user; + } + + /** + * Check if the consumer secret provided for the given user is valid + * + * @since 2.1 + * @param string $keys_consumer_secret + * @param string $consumer_secret + * @return bool + */ + private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) { + return hash_equals( $keys_consumer_secret, $consumer_secret ); + } + + /** + * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer + * has a valid key/secret + * + * @param array $keys + * @param array $params the request parameters + * @throws Exception + */ + private function check_oauth_signature( $keys, $params ) { + $http_method = strtoupper( WC()->api->server->method ); + + $server_path = WC()->api->server->path; + + // if the requested URL has a trailingslash, make sure our base URL does as well + if ( isset( $_SERVER['REDIRECT_URL'] ) && '/' === substr( $_SERVER['REDIRECT_URL'], -1 ) ) { + $server_path .= '/'; + } + + $base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . $server_path ); + + // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature + $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); + unset( $params['oauth_signature'] ); + + // Sort parameters + if ( ! uksort( $params, 'strcmp' ) ) { + throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 ); + } + + // Normalize parameter key/values + $params = $this->normalize_parameters( $params ); + $query_parameters = array(); + foreach ( $params as $param_key => $param_value ) { + if ( is_array( $param_value ) ) { + foreach ( $param_value as $param_key_inner => $param_value_inner ) { + $query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner; + } + } else { + $query_parameters[] = $param_key . '%3D' . $param_value; // join with equals sign + } + } + $query_string = implode( '%26', $query_parameters ); // join with ampersand + + $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; + + if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { + throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 ); + } + + $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); + + $secret = $keys['consumer_secret'] . '&'; + $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); + + if ( ! hash_equals( $signature, $consumer_signature ) ) { + throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 ); + } + } + + /** + * Normalize each parameter by assuming each parameter may have already been + * encoded, so attempt to decode, and then re-encode according to RFC 3986 + * + * Note both the key and value is normalized so a filter param like: + * + * 'filter[period]' => 'week' + * + * is encoded to: + * + * 'filter%5Bperiod%5D' => 'week' + * + * This conforms to the OAuth 1.0a spec which indicates the entire query string + * should be URL encoded + * + * @since 2.1 + * @see rawurlencode() + * @param array $parameters un-normalized pararmeters + * @return array normalized parameters + */ + private function normalize_parameters( $parameters ) { + $keys = WC_API_Authentication::urlencode_rfc3986( array_keys( $parameters ) ); + $values = WC_API_Authentication::urlencode_rfc3986( array_values( $parameters ) ); + $parameters = array_combine( $keys, $values ); + return $parameters; + } + + /** + * Encodes a value according to RFC 3986. Supports multidimensional arrays. + * + * @since 2.4 + * @param string|array $value The value to encode + * @return string|array Encoded values + */ + public static function urlencode_rfc3986( $value ) { + if ( is_array( $value ) ) { + return array_map( array( 'WC_API_Authentication', 'urlencode_rfc3986' ), $value ); + } else { + // Percent symbols (%) must be double-encoded + return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); + } + } + + /** + * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where + * an attacker could attempt to re-send an intercepted request at a later time. + * + * - A timestamp is valid if it is within 15 minutes of now + * - A nonce is valid if it has not been used within the last 15 minutes + * + * @param array $keys + * @param int $timestamp the unix timestamp for when the request was made + * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated + * @throws Exception + */ + private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) { + global $wpdb; + + $valid_window = 15 * 60; // 15 minute window + + if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { + throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 ); + } + + $used_nonces = maybe_unserialize( $keys['nonces'] ); + + if ( empty( $used_nonces ) ) { + $used_nonces = array(); + } + + if ( in_array( $nonce, $used_nonces ) ) { + throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 ); + } + + $used_nonces[ $timestamp ] = $nonce; + + // Remove expired nonces + foreach ( $used_nonces as $nonce_timestamp => $nonce ) { + if ( $nonce_timestamp < ( time() - $valid_window ) ) { + unset( $used_nonces[ $nonce_timestamp ] ); + } + } + + $used_nonces = maybe_serialize( $used_nonces ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'nonces' => $used_nonces ), + array( 'key_id' => $keys['key_id'] ), + array( '%s' ), + array( '%d' ) + ); + } + + /** + * Check that the API keys provided have the proper key-specific permissions to either read or write API resources + * + * @param string $key_permissions + * @throws Exception if the permission check fails + */ + public function check_api_key_permissions( $key_permissions ) { + switch ( WC()->api->server->method ) { + + case 'HEAD': + case 'GET': + if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) { + throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 ); + } + break; + + case 'POST': + case 'PUT': + case 'PATCH': + case 'DELETE': + if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) { + throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 ); + } + break; + } + } + + /** + * Updated API Key last access datetime + * + * @since 2.4.0 + * + * @param int $key_id + */ + private function update_api_key_last_access( $key_id ) { + global $wpdb; + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'last_access' => current_time( 'mysql' ) ), + array( 'key_id' => $key_id ), + array( '%s' ), + array( '%d' ) + ); + } +} diff --git a/includes/api/legacy/v3/class-wc-api-coupons.php b/includes/api/legacy/v3/class-wc-api-coupons.php new file mode 100644 index 00000000000..1e2c9b8651c --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-coupons.php @@ -0,0 +1,574 @@ + + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /coupons + $routes[ $this->base ] = array( + array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ), + array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /coupons/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /coupons/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_coupon' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), + array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ), + ); + + # GET /coupons/code/, note that coupon codes can contain spaces, dashes and underscores + $routes[ $this->base . '/code/(?P\w[\w\s\-]*)' ] = array( + array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ), + ); + + # POST|PUT /coupons/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all coupons + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param int $page + * @return array + */ + public function get_coupons( $fields = null, $filter = array(), $page = 1 ) { + + $filter['page'] = $page; + + $query = $this->query_coupons( $filter ); + + $coupons = array(); + + foreach ( $query->posts as $coupon_id ) { + + if ( ! $this->is_readable( $coupon_id ) ) { + continue; + } + + $coupons[] = current( $this->get_coupon( $coupon_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'coupons' => $coupons ); + } + + /** + * Get the coupon for the given ID + * + * @since 2.1 + * @param int $id the coupon ID + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_coupon( $id, $fields = null ) { + try { + + $id = $this->validate_request( $id, 'shop_coupon', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $coupon = new WC_Coupon( $id ); + + if ( 0 === $coupon->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 ); + } + + $coupon_data = array( + 'id' => $coupon->get_id(), + 'code' => $coupon->get_code(), + 'type' => $coupon->get_discount_type(), + 'created_at' => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times. + 'amount' => wc_format_decimal( $coupon->get_amount(), 2 ), + 'individual_use' => $coupon->get_individual_use(), + 'product_ids' => array_map( 'absint', (array) $coupon->get_product_ids() ), + 'exclude_product_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ), + 'usage_limit' => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null, + 'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null, + 'limit_usage_to_x_items' => (int) $coupon->get_limit_usage_to_x_items(), + 'usage_count' => (int) $coupon->get_usage_count(), + 'expiry_date' => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times. + 'enable_free_shipping' => $coupon->get_free_shipping(), + 'product_category_ids' => array_map( 'absint', (array) $coupon->get_product_categories() ), + 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ), + 'exclude_sale_items' => $coupon->get_exclude_sale_items(), + 'minimum_amount' => wc_format_decimal( $coupon->get_minimum_amount(), 2 ), + 'maximum_amount' => wc_format_decimal( $coupon->get_maximum_amount(), 2 ), + 'customer_emails' => $coupon->get_email_restrictions(), + 'description' => $coupon->get_description(), + ); + + return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of coupons + * + * @since 2.1 + * @param array $filter + * @return array|WP_Error + */ + public function get_coupons_count( $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_shop_coupons' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 ); + } + + $query = $this->query_coupons( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the coupon for the given code + * + * @since 2.1 + * @param string $code the coupon code + * @param string $fields fields to include in response + * @return int|WP_Error + */ + public function get_coupon_by_code( $code, $fields = null ) { + global $wpdb; + + try { + $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) ); + + if ( is_null( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 ); + } + + return $this->get_coupon( $id, $fields ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a coupon + * + * @since 2.2 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_coupon( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['coupon'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 ); + } + + $data = $data['coupon']; + + // Check user permission + if ( ! current_user_can( 'publish_shop_coupons' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this ); + + // Check if coupon code is specified + if ( ! isset( $data['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 ); + } + + $coupon_code = wc_format_coupon_code( $data['code'] ); + $id_from_code = wc_get_coupon_id_by_code( $coupon_code ); + + if ( $id_from_code ) { + throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 ); + } + + $defaults = array( + 'type' => 'fixed_cart', + 'amount' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'exclude_product_ids' => array(), + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => '', + 'usage_count' => '', + 'expiry_date' => '', + 'enable_free_shipping' => false, + 'product_category_ids' => array(), + 'exclude_product_category_ids' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'customer_emails' => array(), + 'description' => '', + ); + + $coupon_data = wp_parse_args( $data, $defaults ); + + // Validate coupon types + if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 ); + } + + $new_coupon = array( + 'post_title' => $coupon_code, + 'post_content' => '', + 'post_status' => 'publish', + 'post_author' => get_current_user_id(), + 'post_type' => 'shop_coupon', + 'post_excerpt' => $coupon_data['description'], + ); + + $id = wp_insert_post( $new_coupon, true ); + + if ( is_wp_error( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 ); + } + + // Set coupon meta + update_post_meta( $id, 'discount_type', $coupon_data['type'] ); + update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) ); + update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' ); + update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) ); + update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) ); + update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) ); + update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) ); + update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) ); + update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) ); + update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) ); + update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) ); + update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' ); + update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) ); + update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) ); + update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' ); + update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) ); + update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) ); + update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) ); + + do_action( 'woocommerce_api_create_coupon', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_coupon( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a coupon + * + * @since 2.2 + * + * @param int $id the coupon ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_coupon( $id, $data ) { + + try { + if ( ! isset( $data['coupon'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 ); + } + + $data = $data['coupon']; + + $id = $this->validate_request( $id, 'shop_coupon', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this ); + + if ( isset( $data['code'] ) ) { + global $wpdb; + + $coupon_code = wc_format_coupon_code( $data['code'] ); + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 ); + } + + $updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) ); + + if ( 0 === $updated ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 ); + } + } + + if ( isset( $data['description'] ) ) { + $updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) ); + + if ( 0 === $updated ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 ); + } + } + + if ( isset( $data['type'] ) ) { + // Validate coupon types + if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 ); + } + update_post_meta( $id, 'discount_type', $data['type'] ); + } + + if ( isset( $data['amount'] ) ) { + update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); + } + + if ( isset( $data['individual_use'] ) ) { + update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); + } + + if ( isset( $data['product_ids'] ) ) { + update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); + } + + if ( isset( $data['exclude_product_ids'] ) ) { + update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); + } + + if ( isset( $data['usage_limit'] ) ) { + update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) ); + } + + if ( isset( $data['usage_limit_per_user'] ) ) { + update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); + } + + if ( isset( $data['limit_usage_to_x_items'] ) ) { + update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); + } + + if ( isset( $data['usage_count'] ) ) { + update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) ); + } + + if ( isset( $data['expiry_date'] ) ) { + update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); + update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) ); + } + + if ( isset( $data['enable_free_shipping'] ) ) { + update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' ); + } + + if ( isset( $data['product_category_ids'] ) ) { + update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) ); + } + + if ( isset( $data['exclude_product_category_ids'] ) ) { + update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) ); + } + + if ( isset( $data['exclude_sale_items'] ) ) { + update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); + } + + if ( isset( $data['minimum_amount'] ) ) { + update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); + } + + if ( isset( $data['maximum_amount'] ) ) { + update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); + } + + if ( isset( $data['customer_emails'] ) ) { + update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) ); + } + + do_action( 'woocommerce_api_edit_coupon', $id, $data ); + + return $this->get_coupon( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a coupon + * + * @since 2.2 + * + * @param int $id the coupon ID + * @param bool $force true to permanently delete coupon, false to move to trash + * + * @return array|int|WP_Error + */ + public function delete_coupon( $id, $force = false ) { + + $id = $this->validate_request( $id, 'shop_coupon', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + do_action( 'woocommerce_api_delete_coupon', $id, $this ); + + return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) ); + } + + /** + * expiry_date format + * + * @since 2.3.0 + * @param string $expiry_date + * @param bool $as_timestamp (default: false) + * @return string|int + */ + protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) { + if ( '' != $expiry_date ) { + if ( $as_timestamp ) { + return strtotime( $expiry_date ); + } + + return date( 'Y-m-d', strtotime( $expiry_date ) ); + } + + return ''; + } + + /** + * Helper method to get coupon post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_coupons( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'shop_coupon', + 'post_status' => 'publish', + ); + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Bulk update or insert coupons + * Accepts an array with coupons in the formats supported by + * WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['coupons'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 ); + } + + $data = $data['coupons']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $coupons = array(); + + foreach ( $data as $_coupon ) { + $coupon_id = 0; + + // Try to get the coupon ID + if ( isset( $_coupon['id'] ) ) { + $coupon_id = intval( $_coupon['id'] ); + } + + if ( $coupon_id ) { + + // Coupon exists / edit coupon + $edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) ); + + if ( is_wp_error( $edit ) ) { + $coupons[] = array( + 'id' => $coupon_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $coupons[] = $edit['coupon']; + } + } else { + + // Coupon don't exists / create coupon + $new = $this->create_coupon( array( 'coupon' => $_coupon ) ); + + if ( is_wp_error( $new ) ) { + $coupons[] = array( + 'id' => $coupon_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $coupons[] = $new['coupon']; + } + } + } + + return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v3/class-wc-api-customers.php b/includes/api/legacy/v3/class-wc-api-customers.php new file mode 100644 index 00000000000..959faabda4b --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-customers.php @@ -0,0 +1,830 @@ + + * GET /customers//orders + * + * @since 2.2 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /customers + $routes[ $this->base ] = array( + array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ), + array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /customers/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ), + ); + + # GET/PUT/DELETE /customers/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ), + array( array( $this, 'edit_customer' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), + array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ), + ); + + # GET /customers/email/ + $routes[ $this->base . '/email/(?P.+)' ] = array( + array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ), + ); + + # GET /customers//downloads + $routes[ $this->base . '/(?P\d+)/downloads' ] = array( + array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ), + ); + + # POST|PUT /customers/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all customers + * + * @since 2.1 + * @param array $fields + * @param array $filter + * @param int $page + * @return array + */ + public function get_customers( $fields = null, $filter = array(), $page = 1 ) { + + $filter['page'] = $page; + + $query = $this->query_customers( $filter ); + + $customers = array(); + + foreach ( $query->get_results() as $user_id ) { + + if ( ! $this->is_readable( $user_id ) ) { + continue; + } + + $customers[] = current( $this->get_customer( $user_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'customers' => $customers ); + } + + /** + * Get the customer for the given ID + * + * @since 2.1 + * @param int $id the customer ID + * @param array $fields + * @return array|WP_Error + */ + public function get_customer( $id, $fields = null ) { + global $wpdb; + + $id = $this->validate_request( $id, 'customer', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $customer = new WC_Customer( $id ); + $last_order = $customer->get_last_order(); + $customer_data = array( + 'id' => $customer->get_id(), + 'created_at' => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times. + 'last_update' => $this->server->format_datetime( $customer->get_date_modified() ? $customer->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times. + 'email' => $customer->get_email(), + 'first_name' => $customer->get_first_name(), + 'last_name' => $customer->get_last_name(), + 'username' => $customer->get_username(), + 'role' => $customer->get_role(), + 'last_order_id' => is_object( $last_order ) ? $last_order->get_id() : null, + 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times. + 'orders_count' => $customer->get_order_count(), + 'total_spent' => wc_format_decimal( $customer->get_total_spent(), 2 ), + 'avatar_url' => $customer->get_avatar_url(), + 'billing_address' => array( + 'first_name' => $customer->get_billing_first_name(), + 'last_name' => $customer->get_billing_last_name(), + 'company' => $customer->get_billing_company(), + 'address_1' => $customer->get_billing_address_1(), + 'address_2' => $customer->get_billing_address_2(), + 'city' => $customer->get_billing_city(), + 'state' => $customer->get_billing_state(), + 'postcode' => $customer->get_billing_postcode(), + 'country' => $customer->get_billing_country(), + 'email' => $customer->get_billing_email(), + 'phone' => $customer->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $customer->get_shipping_first_name(), + 'last_name' => $customer->get_shipping_last_name(), + 'company' => $customer->get_shipping_company(), + 'address_1' => $customer->get_shipping_address_1(), + 'address_2' => $customer->get_shipping_address_2(), + 'city' => $customer->get_shipping_city(), + 'state' => $customer->get_shipping_state(), + 'postcode' => $customer->get_shipping_postcode(), + 'country' => $customer->get_shipping_country(), + ), + ); + + return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) ); + } + + /** + * Get the customer for the given email + * + * @since 2.1 + * + * @param string $email the customer email + * @param array $fields + * + * @return array|WP_Error + */ + public function get_customer_by_email( $email, $fields = null ) { + try { + if ( is_email( $email ) ) { + $customer = get_user_by( 'email', $email ); + if ( ! is_object( $customer ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 ); + } + } else { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 ); + } + + return $this->get_customer( $customer->ID, $fields ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of customers + * + * @since 2.1 + * + * @param array $filter + * + * @return array|WP_Error + */ + public function get_customers_count( $filter = array() ) { + try { + if ( ! current_user_can( 'list_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 ); + } + + $query = $this->query_customers( $filter ); + + return array( 'count' => $query->get_total() ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get customer billing address fields. + * + * @since 2.2 + * @return array + */ + protected function get_customer_billing_address() { + $billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array( + 'first_name', + 'last_name', + 'company', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + 'email', + 'phone', + ) ); + + return $billing_address; + } + + /** + * Get customer shipping address fields. + * + * @since 2.2 + * @return array + */ + protected function get_customer_shipping_address() { + $shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array( + 'first_name', + 'last_name', + 'company', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ) ); + + return $shipping_address; + } + + /** + * Add/Update customer data. + * + * @since 2.2 + * @param int $id the customer ID + * @param array $data + * @param WC_Customer $customer + */ + protected function update_customer_data( $id, $data, $customer ) { + + // Customer first name. + if ( isset( $data['first_name'] ) ) { + $customer->set_first_name( wc_clean( $data['first_name'] ) ); + } + + // Customer last name. + if ( isset( $data['last_name'] ) ) { + $customer->set_last_name( wc_clean( $data['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $data['billing_address'] ) ) { + foreach ( $this->get_customer_billing_address() as $field ) { + if ( isset( $data['billing_address'][ $field ] ) ) { + if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) { + $customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] ); + } else { + $customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) ); + } + } + } + } + + // Customer shipping address. + if ( isset( $data['shipping_address'] ) ) { + foreach ( $this->get_customer_shipping_address() as $field ) { + if ( isset( $data['shipping_address'][ $field ] ) ) { + if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { + $customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] ); + } else { + $customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) ); + } + } + } + } + + do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer ); + } + + /** + * Create a customer + * + * @since 2.2 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_customer( $data ) { + try { + if ( ! isset( $data['customer'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 ); + } + + $data = $data['customer']; + + // Checks with can create new users. + if ( ! current_user_can( 'create_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this ); + + // Checks with the email is missing. + if ( ! isset( $data['email'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 ); + } + + // Create customer. + $customer = new WC_Customer; + $customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' ); + $customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' ); + $customer->set_email( $data['email'] ); + $customer->save(); + + if ( ! $customer->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); + } + + // Added customer data. + $this->update_customer_data( $customer->get_id(), $data, $customer ); + $customer->save(); + + do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data ); + + $this->server->send_status( 201 ); + + return $this->get_customer( $customer->get_id() ); + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a customer + * + * @since 2.2 + * + * @param int $id the customer ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_customer( $id, $data ) { + try { + if ( ! isset( $data['customer'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 ); + } + + $data = $data['customer']; + + // Validate the customer ID. + $id = $this->validate_request( $id, 'customer', 'edit' ); + + // Return the validate error. + if ( is_wp_error( $id ) ) { + throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this ); + + $customer = new WC_Customer( $id ); + + // Customer email. + if ( isset( $data['email'] ) ) { + $customer->set_email( $data['email'] ); + } + + // Customer password. + if ( isset( $data['password'] ) ) { + $customer->set_password( $data['password'] ); + } + + // Update customer data. + $this->update_customer_data( $customer->get_id(), $data, $customer ); + + $customer->save(); + + do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data ); + + return $this->get_customer( $customer->get_id() ); + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a customer + * + * @since 2.2 + * @param int $id the customer ID + * @return array|WP_Error + */ + public function delete_customer( $id ) { + + // Validate the customer ID. + $id = $this->validate_request( $id, 'customer', 'delete' ); + + // Return the validate error. + if ( is_wp_error( $id ) ) { + return $id; + } + + do_action( 'woocommerce_api_delete_customer', $id, $this ); + + return $this->delete( $id, 'customer' ); + } + + /** + * Get the orders for a customer + * + * @since 2.1 + * @param int $id the customer ID + * @param string $fields fields to include in response + * @param array $filter filters + * @return array|WP_Error + */ + public function get_customer_orders( $id, $fields = null, $filter = array() ) { + $id = $this->validate_request( $id, 'customer', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $filter['customer_id'] = $id; + $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, null, -1 ); + + return $orders; + } + + /** + * Get the available downloads for a customer + * + * @since 2.2 + * @param int $id the customer ID + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_customer_downloads( $id, $fields = null ) { + $id = $this->validate_request( $id, 'customer', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $downloads = array(); + $_downloads = wc_get_customer_available_downloads( $id ); + + foreach ( $_downloads as $key => $download ) { + $downloads[] = array( + 'download_url' => $download['download_url'], + 'download_id' => $download['download_id'], + 'product_id' => $download['product_id'], + 'download_name' => $download['download_name'], + 'order_id' => $download['order_id'], + 'order_key' => $download['order_key'], + 'downloads_remaining' => $download['downloads_remaining'], + 'access_expires' => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null, + 'file' => $download['file'], + ); + } + + return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) ); + } + + /** + * Helper method to get customer user objects + * + * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited + * pagination support + * + * The filter for role can only be a single role in a string. + * + * @since 2.3 + * @param array $args request arguments for filtering query + * @return WP_User_Query + */ + private function query_customers( $args = array() ) { + + // default users per page + $users_per_page = get_option( 'posts_per_page' ); + + // Set base query arguments + $query_args = array( + 'fields' => 'ID', + 'role' => 'customer', + 'orderby' => 'registered', + 'number' => $users_per_page, + ); + + // Custom Role + if ( ! empty( $args['role'] ) ) { + $query_args['role'] = $args['role']; + + // Show users on all roles + if ( 'all' === $query_args['role'] ) { + unset( $query_args['role'] ); + } + } + + // Search + if ( ! empty( $args['q'] ) ) { + $query_args['search'] = $args['q']; + } + + // Limit number of users returned + if ( ! empty( $args['limit'] ) ) { + if ( -1 == $args['limit'] ) { + unset( $query_args['number'] ); + } else { + $query_args['number'] = absint( $args['limit'] ); + $users_per_page = absint( $args['limit'] ); + } + } else { + $args['limit'] = $query_args['number']; + } + + // Page + $page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1; + + // Offset + if ( ! empty( $args['offset'] ) ) { + $query_args['offset'] = absint( $args['offset'] ); + } else { + $query_args['offset'] = $users_per_page * ( $page - 1 ); + } + + // Created date + if ( ! empty( $args['created_at_min'] ) ) { + $this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] ); + } + + if ( ! empty( $args['created_at_max'] ) ) { + $this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] ); + } + + // Order (ASC or DESC, ASC by default) + if ( ! empty( $args['order'] ) ) { + $query_args['order'] = $args['order']; + } + + // Orderby + if ( ! empty( $args['orderby'] ) ) { + $query_args['orderby'] = $args['orderby']; + + // Allow sorting by meta value + if ( ! empty( $args['orderby_meta_key'] ) ) { + $query_args['meta_key'] = $args['orderby_meta_key']; + } + } + + $query = new WP_User_Query( $query_args ); + + // Helper members for pagination headers + $query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page ); + $query->page = $page; + + return $query; + } + + /** + * Add customer data to orders + * + * @since 2.1 + * @param $order_data + * @param $order + * @return array + */ + public function add_customer_data( $order_data, $order ) { + + if ( 0 == $order->get_user_id() ) { + + // add customer data from order + $order_data['customer'] = array( + 'id' => 0, + 'email' => $order->get_billing_email(), + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + ); + + } else { + + $order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) ); + } + + return $order_data; + } + + /** + * Modify the WP_User_Query to support filtering on the date the customer was created + * + * @since 2.1 + * @param WP_User_Query $query + */ + public function modify_user_query( $query ) { + + if ( $this->created_at_min ) { + $query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) ); + } + + if ( $this->created_at_max ) { + $query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) ); + } + } + + /** + * Validate the request by checking: + * + * 1) the ID is a valid integer + * 2) the ID returns a valid WP_User + * 3) the current user has the proper permissions + * + * @since 2.1 + * @see WC_API_Resource::validate_request() + * @param integer $id the customer ID + * @param string $type the request type, unused because this method overrides the parent class + * @param string $context the context of the request, either `read`, `edit` or `delete` + * @return int|WP_Error valid user ID or WP_Error if any of the checks fails + */ + protected function validate_request( $id, $type, $context ) { + + try { + $id = absint( $id ); + + // validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 ); + } + + // non-existent IDs return a valid WP_User object with the user ID = 0 + $customer = new WP_User( $id ); + + if ( 0 === $customer->ID ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 ); + } + + // validate permissions + switch ( $context ) { + + case 'read': + if ( ! current_user_can( 'list_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 ); + } + break; + + case 'edit': + if ( ! current_user_can( 'edit_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 ); + } + break; + + case 'delete': + if ( ! current_user_can( 'delete_users' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 ); + } + break; + } + + return $id; + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Check if the current user can read users + * + * @since 2.1 + * @see WC_API_Resource::is_readable() + * @param int|WP_Post $post unused + * @return bool true if the current user can read users, false otherwise + */ + protected function is_readable( $post ) { + return current_user_can( 'list_users' ); + } + + /** + * Bulk update or insert customers + * Accepts an array with customers in the formats supported by + * WC_API_Customers->create_customer() and WC_API_Customers->edit_customer() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['customers'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 ); + } + + $data = $data['customers']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $customers = array(); + + foreach ( $data as $_customer ) { + $customer_id = 0; + + // Try to get the customer ID + if ( isset( $_customer['id'] ) ) { + $customer_id = intval( $_customer['id'] ); + } + + if ( $customer_id ) { + + // Customer exists / edit customer + $edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) ); + + if ( is_wp_error( $edit ) ) { + $customers[] = array( + 'id' => $customer_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $customers[] = $edit['customer']; + } + } else { + + // Customer don't exists / create customer + $new = $this->create_customer( array( 'customer' => $_customer ) ); + + if ( is_wp_error( $new ) ) { + $customers[] = array( + 'id' => $customer_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $customers[] = $new['customer']; + } + } + } + + return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v3/class-wc-api-exception.php b/includes/api/legacy/v3/class-wc-api-exception.php new file mode 100644 index 00000000000..834ed04d6eb --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-exception.php @@ -0,0 +1,48 @@ +error_code = $error_code; + parent::__construct( $error_message, $http_status_code ); + } + + /** + * Returns the error code + * + * @since 2.2 + * @return string + */ + public function getErrorCode() { + return $this->error_code; + } +} diff --git a/includes/api/legacy/v3/class-wc-api-json-handler.php b/includes/api/legacy/v3/class-wc-api-json-handler.php new file mode 100644 index 00000000000..e19ac15d371 --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-json-handler.php @@ -0,0 +1,78 @@ +api->server->send_status( 400 ); + + $data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ); + } + + // Check for invalid characters (only alphanumeric allowed) + if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) { + + WC()->api->server->send_status( 400 ); + + $data = array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ); + } + + // see http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ + WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); + + // Prepend '/**/' to mitigate possible JSONP Flash attacks + return '/**/' . $_GET['_jsonp'] . '(' . json_encode( $data ) . ')'; + } + + return json_encode( $data ); + } +} diff --git a/includes/api/legacy/v3/class-wc-api-orders.php b/includes/api/legacy/v3/class-wc-api-orders.php new file mode 100644 index 00000000000..06a28c6a540 --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-orders.php @@ -0,0 +1,1883 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET|POST /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET /orders/statuses + $routes[ $this->base . '/statuses' ] = array( + array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /orders/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_order' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), + ); + + # GET|POST /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//notes/ + $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( + array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), + ); + + # GET|POST /orders//refunds + $routes[ $this->base . '/(?P\d+)/refunds' ] = array( + array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//refunds/ + $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( + array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /orders/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all orders + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param string $status + * @param int $page + * @return array + */ + public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_orders( $filter ); + + $orders = array(); + + foreach ( $query->posts as $order_id ) { + + if ( ! $this->is_readable( $order_id ) ) { + continue; + } + + $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'orders' => $orders ); + } + + + /** + * Get the order for the given ID. + * + * @since 2.1 + * @param int $id The order ID. + * @param array $fields Request fields. + * @param array $filter Request filters. + * @return array|WP_Error + */ + public function get_order( $id, $fields = null, $filter = array() ) { + + // Ensure order ID is valid & user has permission to read. + $id = $this->validate_request( $id, $this->post_type, 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + // Get the decimal precession. + $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); + $order = wc_get_order( $id ); + $expand = array(); + + if ( ! empty( $filter['expand'] ) ) { + $expand = explode( ',', $filter['expand'] ); + } + + $order_data = array( + 'id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'order_key' => $order->get_order_key(), + 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'status' => $order->get_status(), + 'currency' => $order->get_currency(), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'shipping_methods' => $order->get_shipping_method(), + 'payment_details' => array( + 'method_id' => $order->get_payment_method(), + 'method_title' => $order->get_payment_method_title(), + 'paid' => ! is_null( $order->get_date_paid() ), + ), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + 'note' => $order->get_customer_note(), + 'customer_ip' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'customer_id' => $order->get_user_id(), + 'view_order_url' => $order->get_view_order_url(), + 'line_items' => array(), + 'shipping_lines' => array(), + 'tax_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_item = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + ); + + if ( in_array( 'products', $expand ) && is_object( $product ) ) { + $_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() ); + + if ( isset( $_product_data['product'] ) ) { + $line_item['product_data'] = $_product_data['product']; + } + } + + $order_data['line_items'][] = $line_item; + } + + // Add shipping. + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $order_data['shipping_lines'][] = array( + 'id' => $shipping_item_id, + 'method_id' => $shipping_item->get_method_id(), + 'method_title' => $shipping_item->get_name(), + 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), + ); + } + + // Add taxes. + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $tax_line = array( + 'id' => $tax->id, + 'rate_id' => $tax->rate_id, + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, $dp ), + 'compound' => (bool) $tax->is_compound, + ); + + if ( in_array( 'taxes', $expand ) ) { + $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); + + if ( isset( $_rate_data['tax'] ) ) { + $tax_line['rate_data'] = $_rate_data['tax']; + } + } + + $order_data['tax_lines'][] = $tax_line; + } + + // Add fees. + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $order_data['fee_lines'][] = array( + 'id' => $fee_item_id, + 'title' => $fee_item->get_name(), + 'tax_class' => $fee_item->get_tax_class(), + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + ); + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item->get_code(), + 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), + ); + + if ( in_array( 'coupons', $expand ) ) { + $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() ); + + if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) { + $coupon_line['coupon_data'] = $_coupon_data['coupon']; + } + } + + $order_data['coupon_lines'][] = $coupon_line; + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.4 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + try { + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + + if ( 'any' === $status ) { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $filter['status'] = str_replace( 'wc-', '', $slug ); + $query = $this->query_orders( $filter ); + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; + } + + return array( 'count' => $order_statuses ); + + } else { + $filter['status'] = $status; + } + } + + $query = $this->query_orders( $filter ); + + return array( 'count' => (int) $query->found_posts ); + + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a list of valid order statuses + * + * Note this requires no specific permissions other than being an authenticated + * API user. Order statuses (particularly custom statuses) could be considered + * private information which is why it's not in the API index. + * + * @since 2.1 + * @return array + */ + public function get_order_statuses() { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; + } + + return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); + } + + /** + * Create an order + * + * @since 2.2 + * + * @param array $data raw order data + * + * @return array|WP_Error + */ + public function create_order( $data ) { + global $wpdb; + + wc_transaction_query( 'start' ); + + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); + + // default order args, note that status is checked for validity in wc_create_order() + $default_order_args = array( + 'status' => isset( $data['status'] ) ? $data['status'] : '', + 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, + ); + + // if creating order for existing customer + if ( ! empty( $data['customer_id'] ) ) { + + // make sure customer exists + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $default_order_args['customer_id'] = $data['customer_id']; + } + + // create the pending order + $order = $this->create_base_order( $default_order_args, $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // billing/shipping addresses + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $set_item = "set_{$line_type}"; + + foreach ( $data[ $line ] as $item ) { + + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // set is vat exempt + if ( isset( $data['is_vat_exempt'] ) ) { + update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' ); + } + + // calculate totals and set them + $order->calculate_totals(); + + // payment method (and payment_complete() if `paid` == true) + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // method ID & title are required + if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + update_post_meta( $order->get_id(), '_payment_method_title', $data['payment_details']['method_title'] ); + + // mark as paid if set + if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // set order currency + if ( isset( $data['currency'] ) ) { + + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // set order meta + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); + + wc_transaction_query( 'commit' ); + + return $this->get_order( $order->get_id() ); + + } catch ( WC_Data_Exception $e ) { + wc_transaction_query( 'rollback' ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + wc_transaction_query( 'rollback' ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Creates new WC_Order. + * + * Requires a separate function for classes that extend WC_API_Orders. + * + * @since 2.3 + * + * @param $args array + * @param $data + * + * @return WC_Order + */ + protected function create_base_order( $args, $data ) { + return wc_create_order( $args ); + } + + /** + * Edit an order + * + * @since 2.2 + * @param int $id the order ID + * @param array $data + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + $update_totals = false; + + $id = $this->validate_request( $id, $this->post_type, 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); + $order = wc_get_order( $id ); + + if ( empty( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $order_args = array( 'order_id' => $order->get_id() ); + + // Customer note. + if ( isset( $data['note'] ) ) { + $order_args['customer_note'] = $data['note']; + } + + // Customer ID. + if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); + } + + // Billing/shipping address. + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $update_totals = true; + + foreach ( $data[ $line ] as $item ) { + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Payment method (and payment_complete() if `paid` == true and order needs payment). + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // Method ID. + if ( isset( $data['payment_details']['method_id'] ) ) { + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + } + + // Method title. + if ( isset( $data['payment_details']['method_title'] ) ) { + update_post_meta( $order->get_id(), '_payment_method_title', $data['payment_details']['method_title'] ); + } + + // Mark as paid if set. + if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // Set order currency. + if ( isset( $data['currency'] ) ) { + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update order meta. + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $data['status'] ) ) { + // Refresh the order instance. + $order = wc_get_order( $order->get_id() ); + $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); + } + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); + + return $this->get_order( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, $this->post_type, 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + wc_delete_shop_order_transients( $id ); + + do_action( 'woocommerce_api_delete_order', $id, $this ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + protected function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => $this->post_type, + 'post_status' => array_keys( wc_get_order_statuses() ), + ); + + // add status argument + if ( ! empty( $args['status'] ) ) { + $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); + $statuses = explode( ',', $statuses ); + $query_args['post_status'] = $statuses; + + unset( $args['status'] ); + } + + if ( ! empty( $args['customer_id'] ) ) { + $query_args['meta_query'] = array( + array( + 'key' => '_customer_user', + 'value' => absint( $args['customer_id'] ), + 'compare' => '=', + ), + ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Helper method to set/update the billing & shipping addresses for + * an order + * + * @since 2.1 + * @param \WC_Order $order + * @param array $data + */ + protected function set_order_addresses( $order, $data ) { + + $address_fields = array( + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ); + + $billing_address = $shipping_address = array(); + + // billing address + if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['billing_address'][ $field ] ) ) { + $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); + } + } + + unset( $address_fields['email'] ); + unset( $address_fields['phone'] ); + } + + // shipping address + if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['shipping_address'][ $field ] ) ) { + $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); + } + } + } + + $this->update_address( $order, $billing_address, 'billing' ); + $this->update_address( $order, $shipping_address, 'shipping' ); + + // update user meta + if ( $order->get_user_id() ) { + foreach ( $billing_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); + } + foreach ( $shipping_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); + } + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Helper method to add/update order meta, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @since 2.2 + * @param int $order_id valid order ID + * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format + */ + protected function set_order_meta( $order_id, $order_meta ) { + + foreach ( $order_meta as $meta_key => $meta_value ) { + + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null + * + * Items can be deleted by setting the resource ID to null + * + * @since 2.2 + * @param array $item item provided in the request body + * @return bool true if the item resource ID is null, false otherwise + */ + protected function item_is_null( $item ) { + + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Wrapper method to create/update order items + * + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @since 2.2 + * @param \WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_API_Exception if item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = "set_{$item_type}"; + + // verify provided line item ID is associated with order + if ( 'update' === $action ) { + + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->get_id() ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Create or update a line item + * + * @since 2.2 + * @param \WC_Order $order + * @param array $item line item data + * @param string $action 'create' to add line item or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_line_item( $order, $item, $action ) { + $creating = ( 'create' === $action ); + + // product is always required + if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); + } + + // when updating, ensure product ID provided matches + if ( 'update' === $action ) { + + $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); + } + } + + if ( isset( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } elseif ( isset( $item['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $item['sku'] ); + } + + // variations must each have a key & value + $variation_id = 0; + if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { + foreach ( $item['variations'] as $key => $value ) { + if ( ! $key || ! $value ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); + } + } + $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); + } + + $product = wc_get_product( $variation_id ? $variation_id : $product_id ); + + // must be a valid WC_Product + if ( ! is_object( $product ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // quantity must be positive float + if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // quantity is required when creating + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + // quantity + if ( $creating ) { + $line_item = new WC_Order_Item_Product(); + } else { + $line_item = new WC_Order_Item_Product( $item['id'] ); + } + + $line_item->set_product( $product ); + $line_item->set_order_id( $order->get_id() ); + + if ( isset( $item['quantity'] ) ) { + $line_item->set_quantity( $item['quantity'] ); + } + if ( isset( $item['total'] ) ) { + $line_item->set_total( floatval( $item['total'] ) ); + } elseif ( $creating ) { + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); + $line_item->set_total( $total ); + $line_item->set_subtotal( $total ); + } + if ( isset( $item['total_tax'] ) ) { + $line_item->set_total_tax( floatval( $item['total_tax'] ) ); + } + if ( isset( $item['subtotal'] ) ) { + $line_item->set_subtotal( floatval( $item['subtotal'] ) ); + } + if ( isset( $item['subtotal_tax'] ) ) { + $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); + } + if ( $variation_id ) { + $line_item->set_variation_id( $variation_id ); + $line_item->set_variation( $item['variations'] ); + } + + // Save or add to order. + if ( $creating ) { + $order->add_item( $line_item ); + } else { + $item_id = $line_item->save(); + + if ( ! $item_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Given a product ID & API provided variations, find the correct variation ID to use for calculation + * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass + * the cheapest variation ID but provide other information so we have to look up the variation ID. + * + * @param WC_Product $product Product instance + * @param array $variations + * + * @return int Returns an ID if a valid variation was found for this product + */ + public function get_variation_id( $product, $variations = array() ) { + $variation_id = null; + $variations_normalized = array(); + + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + if ( isset( $variations ) && is_array( $variations ) ) { + // start by normalizing the passed variations + foreach ( $variations as $key => $value ) { + $key = str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ); // from get_attributes in class-wc-api-products.php + $variations_normalized[ $key ] = strtolower( $value ); + } + // now search through each product child and see if our passed variations match anything + foreach ( $product->get_children() as $variation ) { + $meta = array(); + foreach ( get_post_meta( $variation ) as $key => $value ) { + $value = $value[0]; + $key = str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ); + $meta[ $key ] = strtolower( $value ); + } + // if the variation array is a part of the $meta array, we found our match + if ( $this->array_contains( $variations_normalized, $meta ) ) { + $variation_id = $variation; + break; + } + } + } + } + + return $variation_id; + } + + /** + * Utility function to see if the meta array contains data from variations + * + * @param array $needles + * @param array $haystack + * + * @return bool + */ + protected function array_contains( $needles, $haystack ) { + foreach ( $needles as $key => $value ) { + if ( $haystack[ $key ] !== $value ) { + return false; + } + } + return true; + } + + /** + * Create or update an order shipping method + * + * @since 2.2 + * @param \WC_Order $order + * @param array $shipping item data + * @param string $action 'create' to add shipping or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_shipping( $order, $shipping, $action ) { + + // total must be a positive float + if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // method ID is required + if ( ! isset( $shipping['method_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + $item = new WC_Order_Item_Shipping(); + $item->set_order_id( $order->get_id() ); + $item->set_shipping_rate( $rate ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Shipping( $shipping['id'] ); + + if ( isset( $shipping['method_id'] ) ) { + $item->set_method_id( $shipping['method_id'] ); + } + + if ( isset( $shipping['method_title'] ) ) { + $item->set_method_title( $shipping['method_title'] ); + } + + if ( isset( $shipping['total'] ) ) { + $item->set_total( floatval( $shipping['total'] ) ); + } + + $shipping_id = $item->save(); + + if ( ! $shipping_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee + * + * @since 2.2 + * @param \WC_Order $order + * @param array $fee item data + * @param string $action 'create' to add fee or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_fee( $order, $fee, $action ) { + + if ( 'create' === $action ) { + + // fee title is required + if ( ! isset( $fee['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order->get_id() ); + $item->set_name( wc_clean( $fee['title'] ) ); + $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); + + // if taxable, tax class and total are required + if ( ! empty( $fee['taxable'] ) ) { + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $item->set_tax_status( 'taxable' ); + $item->set_tax_class( $fee['tax_class'] ); + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); + } + + if ( isset( $fee['tax_data'] ) ) { + $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); + $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); + } + } + + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Fee( $fee['id'] ); + + if ( isset( $fee['title'] ) ) { + $item->set_name( wc_clean( $fee['title'] ) ); + } + + if ( isset( $fee['tax_class'] ) ) { + $item->set_tax_class( $fee['tax_class'] ); + } + + if ( isset( $fee['total'] ) ) { + $item->set_total( floatval( $fee['total'] ) ); + } + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( floatval( $fee['total_tax'] ) ); + } + + $fee_id = $item->save(); + + if ( ! $fee_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon + * + * @since 2.2 + * @param \WC_Order $order + * @param array $coupon item data + * @param string $action 'create' to add coupon or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_coupon( $order, $coupon, $action ) { + + // coupon amount must be positive float + if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // coupon code is required + if ( empty( $coupon['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $coupon['code'], + 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, + 'discount_tax' => 0, + 'order_id' => $order->get_id(), + ) ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Coupon( $coupon['id'] ); + + if ( isset( $coupon['code'] ) ) { + $item->set_code( $coupon['code'] ); + } + + if ( isset( $coupon['amount'] ) ) { + $item->set_discount( floatval( $coupon['amount'] ) ); + } + + $coupon_id = $item->save(); + + if ( ! $coupon_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $order_id, $fields = null ) { + + // ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $args = array( + 'post_id' => $order_id, + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $order_notes = array(); + + foreach ( $notes as $note ) { + + $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); + } + + /** + * Get an order note for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param string $id order note ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_note( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $order_note = array( + 'id' => $note->comment_ID, + 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false, + ); + + return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order note for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @return WP_Error|array error or created note response data + */ + public function create_order_note( $order_id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); + } + + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); + + // note content is required + if ( ! isset( $data['note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); + } + + $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); + + // create the note + $note_id = $order->add_order_note( $data['note'], $is_customer_note ); + + if ( ! $note_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); + + return $this->get_order_note( $order->get_id(), $note_id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit the order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @param array $data parsed request data + * @return WP_Error|array error or edited note response data + */ + public function edit_order_note( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); + + // Note content + if ( isset( $data['note'] ) ) { + + wp_update_comment( + array( + 'comment_ID' => $note->comment_ID, + 'comment_content' => $data['note'], + ) + ); + } + + // Customer note + if ( isset( $data['customer_note'] ) ) { + + update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); + } + + do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); + + return $this->get_order_note( $order->get_id(), $note->comment_ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_note( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + // Force delete since trashed order notes could not be managed through comments list table + $result = wp_delete_comment( $note->comment_ID, true ); + + if ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); + } + + do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); + + return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the order refunds for an order + * + * @since 2.2 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_refunds( $order_id, $fields = null ) { + + // Ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $refund_items = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $order_id, + 'limit' => -1, + 'return' => 'ids', + ) ); + $order_refunds = array(); + + foreach ( $refund_items as $refund_id ) { + $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); + } + + return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); + } + + /** + * Get an order refund for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param int $id + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_refund( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + $order = wc_get_order( $order_id ); + $refund = wc_get_order( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + $line_items = array(); + + // Add line items + foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_items[] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), + 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), + 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), + ); + } + + $order_refund = array( + 'id' => $refund->get_id(), + 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), + 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), + 'reason' => $refund->get_reason(), + 'line_items' => $line_items, + ); + + return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order refund for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @param bool $api_refund do refund using a payment gateway API + * @return WP_Error|array error or created refund response data + */ + public function create_order_refund( $order_id, $data, $api_refund = true ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); + } + + $order_id = absint( $order_id ); + + if ( empty( $order_id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); + + // Refund amount is required + if ( ! isset( $data['amount'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); + } elseif ( 0 > $data['amount'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); + } + + $data['order_id'] = $order_id; + $data['refund_id'] = 0; + + // Create the refund + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_id ); + + if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit an order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @param array $data parsed request data + * @return WP_Error|array error or edited refund response data + */ + public function edit_order_refund( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure order ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); + + // Update reason + if ( isset( $data['reason'] ) ) { + $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); + + if ( is_wp_error( $updated_refund ) ) { + return $updated_refund; + } + } + + // Update refund amount + if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { + update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); + } + + do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_refund( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure refund ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + wc_delete_shop_order_transients( $order_id ); + + do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); + + return $this->delete( $refund->ID, 'refund', true ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Bulk update or insert orders + * Accepts an array with orders in the formats supported by + * WC_API_Orders->create_order() and WC_API_Orders->edit_order() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['orders'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); + } + + $data = $data['orders']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $orders = array(); + + foreach ( $data as $_order ) { + $order_id = 0; + + // Try to get the order ID + if ( isset( $_order['id'] ) ) { + $order_id = intval( $_order['id'] ); + } + + if ( $order_id ) { + + // Order exists / edit order + $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); + + if ( is_wp_error( $edit ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $orders[] = $edit['order']; + } + } else { + + // Order don't exists / create order + $new = $this->create_order( array( 'order' => $_order ) ); + + if ( is_wp_error( $new ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $orders[] = $new['order']; + } + } + } + + return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v3/class-wc-api-products.php b/includes/api/legacy/v3/class-wc-api-products.php new file mode 100644 index 00000000000..b5cb392e8c5 --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-products.php @@ -0,0 +1,3382 @@ + + * GET /products//reviews + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /products + $routes[ $this->base ] = array( + array( array( $this, 'get_products' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /products/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /products/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_product' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), + ); + + # GET /products//reviews + $routes[ $this->base . '/(?P\d+)/reviews' ] = array( + array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), + ); + + # GET /products//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), + ); + + # GET/POST /products/categories + $routes[ $this->base . '/categories' ] = array( + array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/categories/ + $routes[ $this->base . '/categories/(?P\d+)' ] = array( + array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/tags + $routes[ $this->base . '/tags' ] = array( + array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/tags/ + $routes[ $this->base . '/tags/(?P\d+)' ] = array( + array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/shipping_classes + $routes[ $this->base . '/shipping_classes' ] = array( + array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/shipping_classes/ + $routes[ $this->base . '/shipping_classes/(?P\d+)' ] = array( + array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/attributes + $routes[ $this->base . '/attributes' ] = array( + array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/attributes/ + $routes[ $this->base . '/attributes/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/attributes//terms + $routes[ $this->base . '/attributes/(?P\d+)/terms' ] = array( + array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/attributes//terms/ + $routes[ $this->base . '/attributes/(?P\d+)/terms/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ), + ); + + # POST|PUT /products/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all products + * + * @since 2.1 + * @param string $fields + * @param string $type + * @param array $filter + * @param int $page + * @return array + */ + public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $filter['page'] = $page; + + $query = $this->query_products( $filter ); + + $products = array(); + + foreach ( $query->posts as $product_id ) { + + if ( ! $this->is_readable( $product_id ) ) { + continue; + } + + $products[] = current( $this->get_product( $product_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'products' => $products ); + } + + /** + * Get the product for the given ID + * + * @since 2.1 + * @param int $id the product ID + * @param string $fields + * @return array|WP_Error + */ + public function get_product( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + // add data that applies to every product type + $product_data = $this->get_product_data( $product ); + + // add variations to variable products + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $product_data['variations'] = $this->get_variation_data( $product ); + } + + // add the parent product data to an individual variation + if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) { + $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); + } + + // Add grouped products data + if ( $product->is_type( 'grouped' ) && $product->has_child() ) { + $product_data['grouped_products'] = $this->get_grouped_products_data( $product ); + } + + if ( $product->is_type( 'simple' ) ) { + $parent_id = $product->get_parent_id(); + if ( ! empty( $parent_id ) ) { + $_product = wc_get_product( $parent_id ); + $product_data['parent'] = $this->get_product_data( $_product ); + } + } + + return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); + } + + /** + * Get the total number of products + * + * @since 2.1 + * + * @param string $type + * @param array $filter + * + * @return array|WP_Error + */ + public function get_products_count( $type = null, $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $query = $this->query_products( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product. + * + * @since 2.2 + * + * @param array $data posted data + * + * @return array|WP_Error + */ + public function create_product( $data ) { + $id = 0; + + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + // Check permissions. + if ( ! current_user_can( 'publish_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); + + // Check if product title is specified. + if ( ! isset( $data['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); + } + + // Check product type. + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'simple'; + } + + // Set visible visibility when not sent. + if ( ! isset( $data['catalog_visibility'] ) ) { + $data['catalog_visibility'] = 'visible'; + } + + // Validate the product type. + if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Enable description html tags. + $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; + if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { + + $post_content = $data['description']; + } + + // Enable short description html tags. + $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; + if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { + $post_excerpt = $data['short_description']; + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + $product = new $classname(); + + $product->set_name( wc_clean( $data['title'] ) ); + $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); + $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); + $product->set_description( isset( $data['description'] ) ? $post_content : '' ); + $product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 ); + + if ( ! empty( $data['name'] ) ) { + $product->set_slug( sanitize_title( $data['name'] ) ); + } + + // Attempts to create the new product. + $product->save(); + $id = $product->get_id(); + + // Checks for an error in the product creation. + if ( 0 >= $id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + $product->save(); + + // Save variations. + if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } + + do_action( 'woocommerce_api_create_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + $this->server->send_status( 201 ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product + * + * @since 2.2 + * + * @param int $id the product ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product( $id, $data ) { + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + $id = $this->validate_request( $id, 'product', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); + + // Product title. + if ( isset( $data['title'] ) ) { + $product->set_name( wc_clean( $data['title'] ) ); + } + + // Product name (slug). + if ( isset( $data['name'] ) ) { + $product->set_slug( wc_clean( $data['name'] ) ); + } + + // Product status. + if ( isset( $data['status'] ) ) { + $product->set_status( wc_clean( $data['status'] ) ); + } + + // Product short description. + if ( isset( $data['short_description'] ) ) { + // Enable short description html tags. + $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? $data['short_description'] : wc_clean( $data['short_description'] ); + $product->set_short_description( $post_excerpt ); + } + + // Product description. + if ( isset( $data['description'] ) ) { + // Enable description html tags. + $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? $data['description'] : wc_clean( $data['description'] ); + $product->set_description( $post_content ); + } + + // Validate the product type. + if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Menu order. + if ( isset( $data['menu_order'] ) ) { + $product->set_menu_order( intval( $data['menu_order'] ) ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } else { + // Just sync variations. + $product = WC_Product_Variable::sync( $product, false ); + } + } + + $product->save(); + + do_action( 'woocommerce_api_edit_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product. + * + * @since 2.2 + * + * @param int $id the product ID. + * @param bool $force true to permanently delete order, false to move to trash. + * + * @return array|WP_Error + */ + public function delete_product( $id, $force = false ) { + + $id = $this->validate_request( $id, 'product', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + do_action( 'woocommerce_api_delete_product', $id, $this ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->delete( true ); + } + } elseif ( $product->is_type( 'grouped' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->set_parent_id( 0 ); + $child->save(); + } + } + + $product->delete( true ); + $result = $product->get_id() > 0 ? false : true; + } else { + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); + } else { + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); + } + } + + /** + * Get the reviews for a product + * + * @since 2.1 + * @param int $id the product ID to get reviews for + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_product_reviews( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $comments = get_approved_comments( $id ); + $reviews = array(); + + foreach ( $comments as $comment ) { + + $reviews[] = array( + 'id' => intval( $comment->comment_ID ), + 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), + 'review' => $comment->comment_content, + 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), + 'reviewer_name' => $comment->comment_author, + 'reviewer_email' => $comment->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), + ); + } + + return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); + } + + /** + * Get the orders for a product + * + * @since 2.4.0 + * @param int $id the product ID to get orders for + * @param string fields fields to retrieve + * @param array $filter filters to include in response + * @param string $status the order status to retrieve + * @param $page $page page to retrieve + * @return array|WP_Error + */ + public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { + global $wpdb; + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $id ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $filter = array_merge( $filter, array( + 'in' => implode( ',', $order_ids ), + ) ); + + $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); + + return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); + } + + /** + * Get a listing of product categories + * + * @since 2.2 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_categories( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $product_categories = array(); + + $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); + } + + return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product category for the given ID + * + * @since 2.2 + * + * @param string $id product category term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_category( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_cat' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + // Get category display type + $display_type = get_woocommerce_term_meta( $term_id, 'display_type' ); + + // Get category image + $image = ''; + if ( $image_id = get_woocommerce_term_meta( $term_id, 'thumbnail_id' ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $product_category = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => $image ? esc_url( $image ) : '', + 'count' => intval( $term->count ), + ); + + return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product category. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product category if succeed, otherwise WP_Error + * will be returned + */ + public function create_product_category( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_category'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + 'parent' => 0, + 'display' => 'default', + 'image' => '', + ); + + $data = wp_parse_args( $data['product_category'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this ); + + // Check parent. + $data['parent'] = absint( $data['parent'] ); + if ( $data['parent'] ) { + $parent = get_term_by( 'id', $data['parent'], 'product_cat' ); + if ( ! $parent ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 ); + } + } + + // If value of image is numeric, assume value as image_id. + $image = $data['image']; + $image_id = 0; + if ( is_numeric( $image ) ) { + $image_id = absint( $image ); + } elseif ( ! empty( $image ) ) { + $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); + $image_id = $this->set_product_category_image_as_attachment( $upload ); + } + + $insert = wp_insert_term( $data['name'], 'product_cat', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 ); + } + + $id = $insert['term_id']; + + update_woocommerce_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_woocommerce_term_meta( $id, 'thumbnail_id', $image_id ); + } + + do_action( 'woocommerce_api_create_product_category', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_category( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product category. + * + * @since 2.5.0 + * @param int $id Product category term ID + * @param array $data Posted data + * @return array|WP_Error Product category if succeed, otherwise WP_Error + * will be returned + */ + public function edit_product_category( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_category'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_category']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this ); + $category = $this->get_product_category( $id ); + + if ( is_wp_error( $category ) ) { + return $category; + } + + if ( isset( $data['image'] ) ) { + $image_id = 0; + + // If value of image is numeric, assume value as image_id. + $image = $data['image']; + if ( is_numeric( $image ) ) { + $image_id = absint( $image ); + } elseif ( ! empty( $image ) ) { + $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); + $image_id = $this->set_product_category_image_as_attachment( $upload ); + } + + // In case client supplies invalid image or wants to unset category image. + if ( ! wp_attachment_is_image( $image_id ) ) { + $image_id = ''; + } + } + + $update = wp_update_term( $id, 'product_cat', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_catgory', __( 'Could not edit the category', 'woocommerce' ), 400 ); + } + + if ( ! empty( $data['display'] ) ) { + update_woocommerce_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); + } + + if ( isset( $image_id ) ) { + update_woocommerce_term_meta( $id, 'thumbnail_id', $image_id ); + } + + do_action( 'woocommerce_api_edit_product_category', $id, $data ); + + return $this->get_product_category( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product category. + * + * @since 2.5.0 + * @param int $id Product category term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_category( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_cat' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 ); + } + + // When a term is deleted, delete its meta. + if ( get_option( 'db_version' ) < 34370 ) { + $wpdb->delete( $wpdb->woocommerce_termmeta, array( 'woocommerce_term_id' => $id ), array( '%d' ) ); + } + + do_action( 'woocommerce_api_delete_product_category', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product tags. + * + * @since 2.5.0 + * + * @param string|null $fields Fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_tags( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); + } + + $product_tags = array(); + + $terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_tags[] = current( $this->get_product_tag( $term_id, $fields ) ); + } + + return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product tag for the given ID. + * + * @since 2.5.0 + * + * @param string $id Product tag term ID + * @param string|null $fields Fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_tag( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_tag' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + $tag = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'description' => $term->description, + 'count' => intval( $term->count ), + ); + + return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product tag. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product tag if succeed, otherwise WP_Error + * will be returned + */ + public function create_product_tag( $data ) { + try { + if ( ! isset( $data['product_tag'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + ); + + $data = wp_parse_args( $data['product_tag'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this ); + + $insert = wp_insert_term( $data['name'], 'product_tag', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 ); + } + $id = $insert['term_id']; + + do_action( 'woocommerce_api_create_product_tag', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_tag( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product tag. + * + * @since 2.5.0 + * @param int $id Product tag term ID + * @param array $data Posted data + * @return array|WP_Error Product tag if succeed, otherwise WP_Error + * will be returned + */ + public function edit_product_tag( $id, $data ) { + try { + if ( ! isset( $data['product_tag'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_tag']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this ); + $tag = $this->get_product_tag( $id ); + + if ( is_wp_error( $tag ) ) { + return $tag; + } + + $update = wp_update_term( $id, 'product_tag', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_tag', $id, $data ); + + return $this->get_product_tag( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product tag. + * + * @since 2.5.0 + * @param int $id Product tag term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_tag( $id ) { + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_tag' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_tag', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Helper method to get product post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_products( $args ) { + + // Set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'product', + 'post_status' => 'publish', + 'meta_query' => array(), + ); + + // Taxonomy query to filter products by type, category, tag, shipping class, and + // attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies_arg_map = array( + 'product_type' => 'type', + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Add attribute taxonomy names into the map. + foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) { + $taxonomies_arg_map[ $attribute_name ] = $attribute_name; + } + + // Set tax_query for each passed arg. + foreach ( $taxonomies_arg_map as $tax_name => $arg ) { + if ( ! empty( $args[ $arg ] ) ) { + $terms = explode( ',', $args[ $arg ] ); + + $tax_query[] = array( + 'taxonomy' => $tax_name, + 'field' => 'slug', + 'terms' => $terms, + ); + + unset( $args[ $arg ] ); + } + } + + if ( ! empty( $tax_query ) ) { + $query_args['tax_query'] = $tax_query; + } + + // Filter by specific sku + if ( ! empty( $args['sku'] ) ) { + if ( ! is_array( $query_args['meta_query'] ) ) { + $query_args['meta_query'] = array(); + } + + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $args['sku'], + 'compare' => '=', + ); + + $query_args['post_type'] = array( 'product', 'product_variation' ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Get standard product data that applies to every product type + * + * @since 2.1 + * @param WC_Product|int $product + * + * @return array + */ + private function get_product_data( $product ) { + if ( is_numeric( $product ) ) { + $product = wc_get_product( $product ); + } + + return array( + 'title' => $product->get_name(), + 'id' => $product->get_id(), + 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'downloadable' => $product->is_downloadable(), + 'virtual' => $product->is_virtual(), + 'permalink' => $product->get_permalink(), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : null, + 'price_html' => $product->get_price_html(), + 'taxable' => $product->is_taxable(), + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'managing_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'purchaseable' => $product->is_purchasable(), + 'featured' => $product->is_featured(), + 'visible' => $product->is_visible(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'on_sale' => $product->is_on_sale(), + 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'weight' => $product->get_weight() ? $product->get_weight() : null, + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, + 'description' => wpautop( do_shortcode( $product->get_description() ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'parent_id' => $product->get_parent_id(), + 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), + 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), + 'images' => $this->get_images( $product ), + 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), + 'attributes' => $this->get_attributes( $product ), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'download_type' => 'standard', + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'total_sales' => $product->get_total_sales(), + 'variations' => array(), + 'parent' => array(), + 'grouped_products' => array(), + 'menu_order' => $this->get_product_menu_order( $product ), + ); + } + + /** + * Get product menu order. + * + * @since 2.5.3 + * @param WC_Product $product + * @return int + */ + private function get_product_menu_order( $product ) { + $menu_order = $product->get_menu_order(); + + return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product ); + } + + /** + * Get an individual variation's data. + * + * @since 2.1 + * @param WC_Product $product + * @return array + */ + private function get_variation_data( $product ) { + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + $variation = wc_get_product( $child_id ); + + if ( ! $variation || ! $variation->exists() ) { + continue; + } + + $variations[] = array( + 'id' => $variation->get_id(), + 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), + 'downloadable' => $variation->is_downloadable(), + 'virtual' => $variation->is_virtual(), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null, + 'taxable' => $variation->is_taxable(), + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'managing_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'purchaseable' => $variation->is_purchasable(), + 'visible' => $variation->variation_is_visible(), + 'on_sale' => $variation->is_on_sale(), + 'weight' => $variation->get_weight() ? $variation->get_weight() : null, + 'dimensions' => array( + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $product->get_download_limit(), + 'download_expiry' => (int) $product->get_download_expiry(), + ); + } + + return $variations; + } + + /** + * Get grouped products data + * + * @since 2.5.0 + * @param WC_Product $product + * + * @return array + */ + private function get_grouped_products_data( $product ) { + $products = array(); + + foreach ( $product->get_children() as $child_id ) { + $_product = wc_get_product( $child_id ); + + if ( ! $_product || ! $_product->exists() ) { + continue; + } + + $products[] = $this->get_product_data( $_product ); + + } + + return $products; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { + if ( ! isset( $default_attr['name'] ) ) { + continue; + } + + $taxonomy = sanitize_title( $default_attr['name'] ); + + if ( isset( $default_attr['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + + if ( $_attribute['is_variation'] ) { + $value = ''; + + if ( isset( $default_attr['option'] ) ) { + if ( $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); + } else { + $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); + } + } + + if ( $value ) { + $default_attributes[ $taxonomy ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Save product meta. + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + * @throws WC_API_Exception + */ + protected function save_product_meta( $product, $data ) { + global $wpdb; + + // Virtual. + if ( isset( $data['virtual'] ) ) { + $product->set_virtual( $data['virtual'] ); + } + + // Tax status. + if ( isset( $data['tax_status'] ) ) { + $product->set_tax_status( wc_clean( $data['tax_status'] ) ); + } + + // Tax Class. + if ( isset( $data['tax_class'] ) ) { + $product->set_tax_class( wc_clean( $data['tax_class'] ) ); + } + + // Catalog Visibility. + if ( isset( $data['catalog_visibility'] ) ) { + $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); + } + + // Purchase Note. + if ( isset( $data['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $data['featured'] ) ) { + $product->set_featured( $data['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $data ); + + // SKU. + if ( isset( $data['sku'] ) ) { + $sku = $product->get_sku(); + $new_sku = wc_clean( $data['sku'] ); + + if ( '' == $new_sku ) { + $product->set_sku( '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); + if ( ! $unique_sku ) { + throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + $product->set_sku( $new_sku ); + } + } else { + $product->set_sku( '' ); + } + } + } + + // Attributes. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + + foreach ( $data['attributes'] as $attribute ) { + $is_taxonomy = 0; + $taxonomy = 0; + + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $attribute_slug = sanitize_title( $attribute['name'] ); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + $attribute_slug = sanitize_title( $attribute['slug'] ); + } + + if ( $taxonomy ) { + $is_taxonomy = 1; + } + + if ( $is_taxonomy ) { + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + // Update post terms + if ( taxonomy_exists( $taxonomy ) ) { + wp_set_object_terms( $product->get_id(), $values, $taxonomy ); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $taxonomy ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Array based. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + + // Text based, separate by pipe. + } else { + $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute['name'] ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + + uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); + + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + + } else { + + // Regular Price + if ( isset( $data['regular_price'] ) ) { + $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; + } else { + $regular_price = $product->get_regular_price(); + } + + // Sale Price + if ( isset( $data['sale_price'] ) ) { + $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; + } else { + $sale_price = $product->get_sale_price(); + } + + $product->set_regular_price( $regular_price ); + $product->set_sale_price( $sale_price ); + + if ( isset( $data['sale_price_dates_from'] ) ) { + $date_from = $data['sale_price_dates_from']; + } else { + $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $date_to = $data['sale_price_dates_to']; + } else { + $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; + } + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); + } + + $product->set_date_on_sale_to( $date_to ); + $product->set_date_on_sale_from( $date_from ); + if ( $product->is_on_sale() ) { + $product->set_price( $product->get_sale_price() ); + } else { + $product->set_price( $product->get_regular_price() ); + } + } + + // Product parent ID for groups. + if ( isset( $data['parent_id'] ) ) { + $product->set_parent_id( absint( $data['parent_id'] ) ); + } + + // Sold Individually. + if ( isset( $data['sold_individually'] ) ) { + $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); + } + + // Stock status. + if ( isset( $data['in_stock'] ) ) { + $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock Data. + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $data['managing_stock'] ) ) { + $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; + $product->set_manage_stock( $managing_stock ); + } else { + $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; + } + + // Backorders. + if ( isset( $data['backorders'] ) ) { + if ( 'notify' === $data['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; + } + + $product->set_backorders( $backorders ); + } else { + $backorders = $product->get_backorders(); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( 'yes' == $managing_stock ) { + $product->set_backorders( $backorders ); + + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $data['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_backorders( $backorders ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $data['upsell_ids'] ) ) { + $upsells = array(); + $ids = $data['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + + $product->set_upsell_ids( $upsells ); + } else { + $product->set_upsell_ids( array() ); + } + } + + // Cross sells. + if ( isset( $data['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $data['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + + $product->set_cross_sell_ids( $crosssells ); + } else { + $product->set_cross_sell_ids( array() ); + } + } + + // Product categories. + if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { + $product->set_category_ids( $data['categories'] ); + } + + // Product tags. + if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { + $product->set_tag_ids( $data['tags'] ); + } + + // Downloadable. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; + $product->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; + } + + // Downloadable options. + if ( 'yes' == $is_downloadable ) { + + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $product->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $product->set_download_expiry( $data['download_expiry'] ); + } + } + + // Product url. + if ( $product->is_type( 'external' ) ) { + if ( isset( $data['product_url'] ) ) { + $product->set_product_url( $data['product_url'] ); + } + + if ( isset( $data['button_text'] ) ) { + $product->set_button_text( $data['button_text'] ); + } + } + + // Reviews allowed. + if ( isset( $data['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $data['reviews_allowed'] ); + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $data ); + } + + // Do action for product type + do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); + + return $product; + } + + /** + * Save variations. + * + * @since 2.2 + * + * @param WC_Product $product + * @param array $request + * + * @return bool + * @throws WC_API_Exception + */ + protected function save_variations( $product, $request ) { + global $wpdb; + + $id = $product->get_id(); + $variations = $request['variations']; + $attributes = $product->get_attributes(); + + foreach ( $variations as $menu_order => $data ) { + $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; + $variation = new WC_Product_Variation( $variation_id ); + + // Create initial name and status. + if ( ! $variation->get_slug() ) { + /* translators: 1: variation id 2: product name */ + $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); + $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); + } + + // Parent ID. + $variation->set_parent_id( $product->get_id() ); + + // Menu order. + $variation->set_menu_order( $menu_order ); + + // Status. + if ( isset( $data['visible'] ) ) { + $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { + $image = current( $data['image'] ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->save_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = $data['downloadable']; + $variation->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $variation->get_downloadable(); + } + + // Downloads. + if ( $is_downloadable ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + $manage_stock = (bool) $variation->get_manage_stock(); + if ( isset( $data['managing_stock'] ) ) { + $manage_stock = $data['managing_stock']; + } + $variation->set_manage_stock( $manage_stock ); + + $stock_status = $variation->get_stock_status(); + if ( isset( $data['in_stock'] ) ) { + $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; + } + $variation->set_stock_status( $stock_status ); + + $backorders = $variation->get_backorders(); + if ( isset( $data['backorders'] ) ) { + $backorders = $data['backorders']; + } + $variation->set_backorders( $backorders ); + + if ( $manage_stock ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Description. + if ( isset( $data['description'] ) ) { + $variation->set_description( wp_kses_post( $data['description'] ) ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $_attributes = array(); + + foreach ( $data['attributes'] as $attribute_key => $attribute ) { + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $taxonomy = 0; + $_attribute = array(); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + } + + if ( ! $taxonomy ) { + $taxonomy = sanitize_title( $attribute['name'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + } + + if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = sanitize_title( $_attribute['name'] ); + + if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters. + $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; + } else { + $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + } + + $_attributes[ $_attribute_key ] = $_attribute_value; + } + } + + $variation->set_attributes( $_attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); + } + + return true; + } + + /** + * Save product shipping data + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + */ + private function save_product_shipping_data( $product, $data ) { + if ( isset( $data['weight'] ) ) { + $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Product dimensions + if ( isset( $data['dimensions'] ) ) { + // Height + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); + } + + // Width + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); + } + + // Length + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); + } + } + + // Virtual + if ( isset( $data['virtual'] ) ) { + $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; + + if ( 'yes' == $virtual ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } + } + + // Shipping class + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + if ( $shipping_class_id ) { + $product->set_shipping_class_id( $shipping_class_id ); + } + } + + return $product; + } + + /** + * Save downloadable files + * + * @since 2.2 + * @param WC_Product $product + * @param array $downloads + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Get attribute taxonomy by slug. + * + * @since 2.2 + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * Get the images for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_images( $product ) { + $images = $attachment_ids = array(); + $product_image = $product->get_image_id(); + + // Add featured image. + if ( ! empty( $product_image ) ) { + $attachment_ids[] = $product_image; + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + + $attachment_post = get_post( $attachment_id ); + + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'title' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + + $images[] = array( + 'id' => 0, + 'created_at' => $this->server->format_datetime( time() ), // Default to now. + 'updated_at' => $this->server->format_datetime( time() ), + 'src' => wc_placeholder_img_src(), + 'title' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Save product images. + * + * @since 2.2 + * @param WC_Product $product + * @param array $images + * @throws WC_API_Exception + * @return WC_Product + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + if ( isset( $image['position'] ) && 0 == $image['position'] ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $product->set_image_id( $attachment_id ); + } else { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) && $attachment_id ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image title if present. + if ( ! empty( $image['title'] ) && $attachment_id ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Upload image from URL + * + * @since 2.2 + * @param string $image_url + * @return int|WP_Error attachment id + */ + public function upload_product_image( $image_url ) { + return $this->upload_image_from_url( $image_url, 'product_image' ); + } + + /** + * Upload product category image from URL. + * + * @since 2.5.0 + * @param string $image_url + * @return int|WP_Error attachment id + */ + public function upload_product_category_image( $image_url ) { + return $this->upload_image_from_url( $image_url, 'product_category_image' ); + } + + /** + * Upload image from URL. + * + * @throws WC_API_Exception + * + * @since 2.5.0 + * @param string $image_url + * @param string $upload_for + * @return array|WP_Error + */ + protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) { + $file_name = basename( current( explode( '?', $image_url ) ) ); + $parsed_url = @parse_url( $image_url ); + + // Check parsed URL. + if ( ! $parsed_url || ! is_array( $parsed_url ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_' . $upload_for, sprintf( __( 'Invalid URL %s.', 'woocommerce' ), $image_url ), 400 ); + } + + // Ensure url is valid. + $image_url = str_replace( ' ', '%20', $image_url ); + + // Get the file. + $response = wp_safe_remote_get( $image_url, array( + 'timeout' => 10, + ) ); + + if ( is_wp_error( $response ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_remote_' . $upload_for, sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' . sprintf( __( 'Error: %s.', 'woocommerce' ), $response->get_error_message() ), 400 ); + } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_remote_' . $upload_for, sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ), 400 ); + } + + // Ensure we have a file name and type. + $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() ); + + if ( ! $wp_filetype['type'] ) { + $headers = wp_remote_retrieve_headers( $response ); + if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) { + $disposition = end( explode( 'filename=', $headers['content-disposition'] ) ); + $disposition = sanitize_file_name( $disposition ); + $file_name = $disposition; + } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) { + $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] ); + } + unset( $headers ); + + // Recheck filetype + $wp_filetype = wp_check_filetype( $file_name, wc_rest_allowed_image_mime_types() ); + + if ( ! $wp_filetype['type'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_' . $upload_for, __( 'Invalid image type.', 'woocommerce' ), 400 ); + } + } + + // Upload the file. + $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) ); + + if ( $upload['error'] ) { + throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload['error'], 400 ); + } + + // Get filesize. + $filesize = filesize( $upload['file'] ); + + if ( 0 == $filesize ) { + @unlink( $upload['file'] ); + unset( $upload ); + throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_file_error', __( 'Zero size file downloaded.', 'woocommerce' ), 400 ); + } + + unset( $response ); + + do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for ); + + return $upload; + } + + /** + * Sets product image as attachment and returns the attachment ID. + * + * @since 2.2 + * @param array $upload + * @param int $id + * @return int + */ + protected function set_product_image_as_attachment( $upload, $id ) { + return $this->set_uploaded_image_as_attachment( $upload, $id ); + } + + /** + * Sets uploaded category image as attachment and returns the attachment ID. + * + * @since 2.5.0 + * @param integer $upload Upload information from wp_upload_bits + * @return int Attachment ID + */ + protected function set_product_category_image_as_attachment( $upload ) { + return $this->set_uploaded_image_as_attachment( $upload ); + } + + /** + * Set uploaded image as attachment. + * + * @since 2.5.0 + * @param array $upload Upload information from wp_upload_bits + * @param int $id Post ID. Default to 0. + * @return int Attachment ID + */ + protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = wc_clean( $image_meta['title'] ); + } + if ( trim( $image_meta['caption'] ) ) { + $content = wc_clean( $image_meta['caption'] ); + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title, + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; + } + + /** + * Get attribute options. + * + * @param int $product_id + * @param array $attribute + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_attributes( $product ) { + + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + + // variation attributes + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + + // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` + $attributes[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), + 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => wc_attribute_label( $attribute['name'], $product ), + 'slug' => str_replace( 'pa_', '', $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get the downloads for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_downloads( $product ) { + + $downloads = array(); + + if ( $product->is_downloadable() ) { + + foreach ( $product->get_downloads() as $file_id => $file ) { + + $downloads[] = array( + 'id' => $file_id, // do not cast as int as this is a hash + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get a listing of product attributes + * + * @since 2.5.0 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attributes( $fields = null ) { + try { + // Permissions check. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $product_attributes = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $attribute ) { + $product_attributes[] = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + } + + return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute for the given ID + * + * @since 2.5.0 + * + * @param string $id product attribute term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attribute( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $product_attribute = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + + return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Validate attribute data. + * + * @since 2.5.0 + * @param string $name + * @param string $slug + * @param string $type + * @param string $order_by + * @param bool $new_data + * @return bool + * @throws WC_API_Exception + */ + protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { + if ( empty( $name ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + if ( strlen( $slug ) >= 28 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } + + // Validate the attribute type + if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); + } + + // Validate the attribute order by + if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); + } + + return true; + } + + /** + * Create a new product attribute. + * + * @since 2.5.0 + * + * @param array $data Posted data. + * + * @return array|WP_Error + */ + public function create_product_attribute( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $data = $data['product_attribute']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); + + if ( ! isset( $data['name'] ) ) { + $data['name'] = ''; + } + + // Set the attribute slug. + if ( ! isset( $data['slug'] ) ) { + $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); + } else { + $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); + } + + // Set attribute type when not sent. + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'select'; + } + + // Set order by when not sent. + if ( ! isset( $data['order_by'] ) ) { + $data['order_by'] = 'menu_order'; + } + + // Validate the attribute data. + $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $data['name'], + 'attribute_name' => $data['slug'], + 'attribute_type' => $data['type'], + 'attribute_orderby' => $data['order_by'], + 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, + ), + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for an error in the product creation. + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); + } + + $id = $wpdb->insert_id; + + do_action( 'woocommerce_api_create_product_attribute', $id, $data ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute. + * + * @since 2.5.0 + * + * @param int $id the attribute ID. + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); + $attribute = $this->get_product_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; + $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; + $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; + + if ( isset( $data['slug'] ) ) { + $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); + } else { + $attribute_slug = $attribute['product_attribute']['slug']; + } + $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); + + if ( isset( $data['has_archives'] ) ) { + $attribute_public = true === $data['has_archives'] ? 1 : 0; + } else { + $attribute_public = $attribute['product_attribute']['has_archives']; + } + + // Validate the attribute data. + $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $attribute_name, + 'attribute_name' => $attribute_slug, + 'attribute_type' => $attribute_type, + 'attribute_orderby' => $attribute_order_by, + 'attribute_public' => $attribute_public, + ), + array( 'attribute_id' => $id ), + array( '%s', '%s', '%s', '%s', '%d' ), + array( '%d' ) + ); + + // Checks for an error in the product creation. + if ( false === $update ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute. + * + * @since 2.5.0 + * + * @param int $id the product attribute ID. + * + * @return array|WP_Error + */ + public function delete_product_attribute( $id ) { + global $wpdb; + + try { + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + $attribute_name = $wpdb->get_var( $wpdb->prepare( " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_null( $attribute_name ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); + do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product attribute terms. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param string|null $fields Fields to limit response to. + * + * @return array|WP_Error + */ + public function get_product_attribute_terms( $attribute_id, $fields = null ) { + try { + // Permissions check. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $args = array( 'hide_empty' => false ); + $orderby = wc_attribute_orderby( $taxonomy ); + + switch ( $orderby ) { + case 'name' : + $args['orderby'] = 'name'; + $args['menu_order'] = false; + break; + case 'id' : + $args['orderby'] = 'id'; + $args['order'] = 'ASC'; + $args['menu_order'] = false; + break; + case 'menu_order' : + $args['menu_order'] = 'ASC'; + break; + } + + $terms = get_terms( $taxonomy, $args ); + $attribute_terms = array(); + + foreach ( $terms as $term ) { + $attribute_terms[] = array( + 'id' => $term->term_id, + 'slug' => $term->slug, + 'name' => $term->name, + 'count' => $term->count, + ); + } + + return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute term for the given ID. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param string $id Product attribute term ID. + * @param string|null $fields Fields to limit response to. + * + * @return array|WP_Error + */ + public function get_product_attribute_term( $attribute_id, $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term = get_term( $id, $taxonomy ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $attribute_term = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'count' => $term->count, + ); + + return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param array $data Posted data. + * + * @return array|WP_Error + */ + public function create_product_attribute_term( $attribute_id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute_term'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); + } + + $data = $data['product_attribute_term']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this ); + + // Check if attribute term name is specified. + if ( ! isset( $data['name'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + $args = array(); + + // Set the attribute term slug. + if ( isset( $data['slug'] ) ) { + $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); + } + + $term = wp_insert_term( $data['name'], $taxonomy, $args ); + + // Checks for an error in the term creation. + if ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 ); + } + + $id = $term['term_id']; + + do_action( 'woocommerce_api_create_product_attribute_term', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute_term( $attribute_id, $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param int $id the attribute ID. + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute_term( $attribute_id, $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute_term'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute_term']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this ); + + $args = array(); + + // Update name. + if ( isset( $data['name'] ) ) { + $args['name'] = wc_clean( wp_unslash( $data['name'] ) ); + } + + // Update slug. + if ( isset( $data['slug'] ) ) { + $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); + } + + $term = wp_update_term( $id, $taxonomy, $args ); + + if ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data ); + + return $this->get_product_attribute_term( $attribute_id, $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param int $id the product attribute ID. + * + * @return array|WP_Error + */ + public function delete_product_attribute_term( $attribute_id, $id ) { + global $wpdb; + + try { + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $id = absint( $id ); + $term = wp_delete_term( $id, $taxonomy ); + + if ( ! $term ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 ); + } elseif ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear product + * + * @param int $product_id + */ + protected function clear_product( $product_id ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + // Delete product attachments + $attachments = get_children( array( + 'post_parent' => $product_id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product + $product = wc_get_product( $product_id ); + $product->delete( true ); + } + + /** + * Bulk update or insert products + * Accepts an array with products in the formats supported by + * WC_API_Products->create_product() and WC_API_Products->edit_product() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['products'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); + } + + $data = $data['products']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $products = array(); + + foreach ( $data as $_product ) { + $product_id = 0; + $product_sku = ''; + + // Try to get the product ID + if ( isset( $_product['id'] ) ) { + $product_id = intval( $_product['id'] ); + } + + if ( ! $product_id && isset( $_product['sku'] ) ) { + $product_sku = wc_clean( $_product['sku'] ); + $product_id = wc_get_product_id_by_sku( $product_sku ); + } + + if ( $product_id ) { + + // Product exists / edit product + $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); + + if ( is_wp_error( $edit ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $products[] = $edit['product']; + } + } else { + + // Product don't exists / create product + $new = $this->create_product( array( 'product' => $_product ) ); + + if ( is_wp_error( $new ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $products[] = $new['product']; + } + } + } + + return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product shipping classes. + * + * @since 2.5.0 + * @param string|null $fields Fields to limit response to + * @return array|WP_Error List of product shipping classes if succeed, + * otherwise WP_Error will be returned + */ + public function get_product_shipping_classes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); + } + + $product_shipping_classes = array(); + + $terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) ); + } + + return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product shipping class for the given ID. + * + * @since 2.5.0 + * @param string $id Product shipping class term ID + * @param string|null $fields Fields to limit response to + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function get_product_shipping_class( $id, $fields = null ) { + try { + $id = absint( $id ); + if ( ! $id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_shipping_class' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + $product_shipping_class = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'count' => intval( $term->count ), + ); + + return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product shipping class. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function create_product_shipping_class( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_shipping_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + 'parent' => 0, + ); + + $data = wp_parse_args( $data['product_shipping_class'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this ); + + // Check parent. + $data['parent'] = absint( $data['parent'] ); + if ( $data['parent'] ) { + $parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' ); + if ( ! $parent ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 ); + } + } + + $insert = wp_insert_term( $data['name'], 'product_shipping_class', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 ); + } + + $id = $insert['term_id']; + + do_action( 'woocommerce_api_create_product_shipping_class', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_shipping_class( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product shipping class. + * + * @since 2.5.0 + * @param int $id Product shipping class term ID + * @param array $data Posted data + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function edit_product_shipping_class( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_shipping_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_shipping_class']; + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this ); + $shipping_class = $this->get_product_shipping_class( $id ); + + if ( is_wp_error( $shipping_class ) ) { + return $shipping_class; + } + + $update = wp_update_term( $id, 'product_shipping_class', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data ); + + return $this->get_product_shipping_class( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product shipping class. + * + * @since 2.5.0 + * @param int $id Product shipping class term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_shipping_class( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_shipping_class' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v3/class-wc-api-reports.php b/includes/api/legacy/v3/class-wc-api-reports.php new file mode 100644 index 00000000000..2d591af9077 --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-reports.php @@ -0,0 +1,329 @@ +base ] = array( + array( array( $this, 'get_reports' ), WC_API_Server::READABLE ), + ); + + # GET /reports/sales + $routes[ $this->base . '/sales' ] = array( + array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ), + ); + + # GET /reports/sales/top_sellers + $routes[ $this->base . '/sales/top_sellers' ] = array( + array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ), + ); + + return $routes; + } + + /** + * Get a simple listing of available reports + * + * @since 2.1 + * @return array + */ + public function get_reports() { + return array( 'reports' => array( 'sales', 'sales/top_sellers' ) ); + } + + /** + * Get the sales report + * + * @since 2.1 + * @param string $fields fields to include in response + * @param array $filter date filtering + * @return array|WP_Error + */ + public function get_sales_report( $fields = null, $filter = array() ) { + + // check user permissions + $check = $this->validate_request(); + + // check for WP_Error + if ( is_wp_error( $check ) ) { + return $check; + } + + // set date filtering + $this->setup_report( $filter ); + + // new customers + $users_query = new WP_User_Query( + array( + 'fields' => array( 'user_registered' ), + 'role' => 'customer', + ) + ); + + $customers = $users_query->get_results(); + + foreach ( $customers as $key => $customer ) { + if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { + unset( $customers[ $key ] ); + } + } + + $total_customers = count( $customers ); + $report_data = $this->report->get_report_data(); + $period_totals = array(); + + // setup period totals by ensuring each period in the interval has data + for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { + + switch ( $this->report->chart_groupby ) { + case 'day' : + $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); + break; + default : + $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); + break; + } + + // set the customer signups for each period + $customer_count = 0; + foreach ( $customers as $customer ) { + if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { + $customer_count++; + } + } + + $period_totals[ $time ] = array( + 'sales' => wc_format_decimal( 0.00, 2 ), + 'orders' => 0, + 'items' => 0, + 'tax' => wc_format_decimal( 0.00, 2 ), + 'shipping' => wc_format_decimal( 0.00, 2 ), + 'discount' => wc_format_decimal( 0.00, 2 ), + 'customers' => $customer_count, + ); + } + + // add total sales, total order count, total tax and total shipping for each period + foreach ( $report_data->orders as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); + $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); + $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); + } + + foreach ( $report_data->order_counts as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['orders'] = (int) $order->count; + } + + // add total order items for each period + foreach ( $report_data->order_items as $order_item ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; + } + + // add total discount for each period + foreach ( $report_data->coupons as $discount ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); + } + + $sales_data = array( + 'total_sales' => $report_data->total_sales, + 'net_sales' => $report_data->net_sales, + 'average_sales' => $report_data->average_sales, + 'total_orders' => $report_data->total_orders, + 'total_items' => $report_data->total_items, + 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), + 'total_shipping' => $report_data->total_shipping, + 'total_refunds' => $report_data->total_refunds, + 'total_discount' => $report_data->total_coupons, + 'totals_grouped_by' => $this->report->chart_groupby, + 'totals' => $period_totals, + 'total_customers' => $total_customers, + ); + + return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) ); + } + + /** + * Get the top sellers report + * + * @since 2.1 + * @param string $fields fields to include in response + * @param array $filter date filtering + * @return array|WP_Error + */ + public function get_top_sellers_report( $fields = null, $filter = array() ) { + + // check user permissions + $check = $this->validate_request(); + + if ( is_wp_error( $check ) ) { + return $check; + } + + // set date filtering + $this->setup_report( $filter ); + + $top_sellers = $this->report->get_order_report_data( array( + 'data' => array( + '_product_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => '', + 'name' => 'product_id', + ), + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ), + ), + 'order_by' => 'order_item_qty DESC', + 'group_by' => 'product_id', + 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + $top_sellers_data = array(); + + foreach ( $top_sellers as $top_seller ) { + + $product = wc_get_product( $top_seller->product_id ); + + if ( $product ) { + $top_sellers_data[] = array( + 'title' => $product->get_name(), + 'product_id' => $top_seller->product_id, + 'quantity' => $top_seller->order_item_qty, + ); + } + } + + return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) ); + } + + /** + * Setup the report object and parse any date filtering + * + * @since 2.1 + * @param array $filter date filtering + */ + private function setup_report( $filter ) { + + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); + + $this->report = new WC_Report_Sales_By_Date(); + + if ( empty( $filter['period'] ) ) { + + // custom date range + $filter['period'] = 'custom'; + + if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { + + // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges + $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] ); + $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null; + + } else { + + // default custom range to today + $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); + } + } else { + + // ensure period is valid + if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { + $filter['period'] = 'week'; + } + + // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods + // allow "week" for period instead of "7day" + if ( 'week' === $filter['period'] ) { + $filter['period'] = '7day'; + } + } + + $this->report->calculate_current_range( $filter['period'] ); + } + + /** + * Verify that the current user has permission to view reports + * + * @since 2.1 + * @see WC_API_Resource::validate_request() + * + * @param null $id unused + * @param null $type unused + * @param null $context unused + * + * @return true|WP_Error + */ + protected function validate_request( $id = null, $type = null, $context = null ) { + + if ( ! current_user_can( 'view_woocommerce_reports' ) ) { + + return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) ); + + } else { + + return true; + } + } +} diff --git a/includes/api/legacy/v3/class-wc-api-resource.php b/includes/api/legacy/v3/class-wc-api-resource.php new file mode 100644 index 00000000000..4fefab1d0fb --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-resource.php @@ -0,0 +1,472 @@ +server = $server; + + // automatically register routes for sub-classes + add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); + + // maybe add meta to top-level resource responses + foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { + add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); + } + + $response_names = array( + 'order', + 'coupon', + 'customer', + 'product', + 'report', + 'customer_orders', + 'customer_downloads', + 'order_note', + 'order_refund', + 'product_reviews', + 'product_category', + 'tax', + 'tax_class', + ); + + foreach ( $response_names as $name ) { + + /** + * Remove fields from responses when requests specify certain fields + * note these are hooked at a later priority so data added via + * filters (e.g. customer data to the order response) still has the + * fields filtered properly + */ + add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 ); + } + } + + /** + * Validate the request by checking: + * + * 1) the ID is a valid integer + * 2) the ID returns a valid post object and matches the provided post type + * 3) the current user has the proper permissions to read/edit/delete the post + * + * @since 2.1 + * @param string|int $id the post ID + * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` + * @param string $context the context of the request, either `read`, `edit` or `delete` + * @return int|WP_Error valid post ID or WP_Error if any of the checks fails + */ + protected function validate_request( $id, $type, $context ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); + } + + // Only custom post types have per-post type/permission checks + if ( 'customer' !== $type ) { + + $post = get_post( $id ); + + if ( null === $post ) { + return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) ); + } + + // For checking permissions, product variations are the same as the product post type + $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; + + // Validate post type + if ( $type !== $post_type ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); + } + + // Validate permissions + switch ( $context ) { + + case 'read': + if ( ! $this->is_readable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'edit': + if ( ! $this->is_editable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'delete': + if ( ! $this->is_deletable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + } + } + + return $id; + } + + /** + * Add common request arguments to argument list before WP_Query is run + * + * @since 2.1 + * @param array $base_args required arguments for the query (e.g. `post_type`, etc) + * @param array $request_args arguments provided in the request + * @return array + */ + protected function merge_query_args( $base_args, $request_args ) { + + $args = array(); + + // date + if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { + + $args['date_query'] = array(); + + // resources created after specified date + if ( ! empty( $request_args['created_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); + } + + // resources created before specified date + if ( ! empty( $request_args['created_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); + } + + // resources updated after specified date + if ( ! empty( $request_args['updated_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); + } + + // resources updated before specified date + if ( ! empty( $request_args['updated_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); + } + } + + // search + if ( ! empty( $request_args['q'] ) ) { + $args['s'] = $request_args['q']; + } + + // resources per response + if ( ! empty( $request_args['limit'] ) ) { + $args['posts_per_page'] = $request_args['limit']; + } + + // resource offset + if ( ! empty( $request_args['offset'] ) ) { + $args['offset'] = $request_args['offset']; + } + + // order (ASC or DESC, ASC by default) + if ( ! empty( $request_args['order'] ) ) { + $args['order'] = $request_args['order']; + } + + // orderby + if ( ! empty( $request_args['orderby'] ) ) { + $args['orderby'] = $request_args['orderby']; + + // allow sorting by meta value + if ( ! empty( $request_args['orderby_meta_key'] ) ) { + $args['meta_key'] = $request_args['orderby_meta_key']; + } + } + + // allow post status change + if ( ! empty( $request_args['post_status'] ) ) { + $args['post_status'] = $request_args['post_status']; + unset( $request_args['post_status'] ); + } + + // filter by a list of post id + if ( ! empty( $request_args['in'] ) ) { + $args['post__in'] = explode( ',', $request_args['in'] ); + unset( $request_args['in'] ); + } + + // exclude by a list of post id + if ( ! empty( $request_args['not_in'] ) ) { + $args['post__not_in'] = explode( ',', $request_args['not_in'] ); + unset( $request_args['not_in'] ); + } + + // resource page + $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; + + $args = apply_filters( 'woocommerce_api_query_args', $args, $request_args ); + + return array_merge( $base_args, $args ); + } + + /** + * Add meta to resources when requested by the client. Meta is added as a top-level + * `_meta` attribute (e.g. `order_meta`) as a list of key/value pairs + * + * @since 2.1 + * @param array $data the resource data + * @param object $resource the resource object (e.g WC_Order) + * @return mixed + */ + public function maybe_add_meta( $data, $resource ) { + + if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { + + // don't attempt to add meta more than once + if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { + return $data; + } + + // define the top-level property name for the meta + switch ( get_class( $resource ) ) { + + case 'WC_Order': + $meta_name = 'order_meta'; + break; + + case 'WC_Coupon': + $meta_name = 'coupon_meta'; + break; + + case 'WP_User': + $meta_name = 'customer_meta'; + break; + + default: + $meta_name = 'product_meta'; + break; + } + + if ( is_a( $resource, 'WP_User' ) ) { + + // customer meta + $meta = (array) get_user_meta( $resource->ID ); + + } else { + + // coupon/order/product meta + $meta = (array) get_post_meta( $resource->get_id() ); + } + + foreach ( $meta as $meta_key => $meta_value ) { + + // don't add hidden meta by default + if ( ! is_protected_meta( $meta_key ) ) { + $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); + } + } + } + + return $data; + } + + /** + * Restrict the fields included in the response if the request specified certain only certain fields should be returned + * + * @since 2.1 + * @param array $data the response data + * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order + * @param array|string the requested list of fields to include in the response + * @return array response data + */ + public function filter_response_fields( $data, $resource, $fields ) { + + if ( ! is_array( $data ) || empty( $fields ) ) { + return $data; + } + + $fields = explode( ',', $fields ); + $sub_fields = array(); + + // get sub fields + foreach ( $fields as $field ) { + + if ( false !== strpos( $field, '.' ) ) { + + list( $name, $value ) = explode( '.', $field ); + + $sub_fields[ $name ] = $value; + } + } + + // iterate through top-level fields + foreach ( $data as $data_field => $data_value ) { + + // if a field has sub-fields and the top-level field has sub-fields to filter + if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { + + // iterate through each sub-field + foreach ( $data_value as $sub_field => $sub_field_value ) { + + // remove non-matching sub-fields + if ( ! in_array( $sub_field, $sub_fields ) ) { + unset( $data[ $data_field ][ $sub_field ] ); + } + } + } else { + + // remove non-matching top-level fields + if ( ! in_array( $data_field, $fields ) ) { + unset( $data[ $data_field ] ); + } + } + } + + return $data; + } + + /** + * Delete a given resource + * + * @since 2.1 + * @param int $id the resource ID + * @param string $type the resource post type, or `customer` + * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) + * @return array|WP_Error + */ + protected function delete( $id, $type, $force = false ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + if ( 'customer' === $type ) { + + $result = wp_delete_user( $id ); + + if ( $result ) { + return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); + } else { + return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); + } + } else { + + // delete order/coupon/product/webhook + $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); + + if ( ! $result ) { + return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); + + } else { + + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); + } + } + } + + + /** + * Checks if the given post is readable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_readable( $post ) { + + return $this->check_permission( $post, 'read' ); + } + + /** + * Checks if the given post is editable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_editable( $post ) { + + return $this->check_permission( $post, 'edit' ); + + } + + /** + * Checks if the given post is deletable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_deletable( $post ) { + + return $this->check_permission( $post, 'delete' ); + } + + /** + * Checks the permissions for the current user given a post and context + * + * @since 2.1 + * @param WP_Post|int $post + * @param string $context the type of permission to check, either `read`, `write`, or `delete` + * @return bool true if the current user has the permissions to perform the context on the post + */ + private function check_permission( $post, $context ) { + $permission = false; + + if ( ! is_a( $post, 'WP_Post' ) ) { + $post = get_post( $post ); + } + + if ( is_null( $post ) ) { + return $permission; + } + + $post_type = get_post_type_object( $post->post_type ); + + if ( 'read' === $context ) { + $permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); + } elseif ( 'edit' === $context ) { + $permission = current_user_can( $post_type->cap->edit_post, $post->ID ); + } elseif ( 'delete' === $context ) { + $permission = current_user_can( $post_type->cap->delete_post, $post->ID ); + } + + return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type ); + } +} diff --git a/includes/api/legacy/v3/class-wc-api-server.php b/includes/api/legacy/v3/class-wc-api-server.php new file mode 100644 index 00000000000..2e8aaaa6435 --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-server.php @@ -0,0 +1,776 @@ + self::METHOD_GET, + 'GET' => self::METHOD_GET, + 'POST' => self::METHOD_POST, + 'PUT' => self::METHOD_PUT, + 'PATCH' => self::METHOD_PATCH, + 'DELETE' => self::METHOD_DELETE, + ); + + /** + * Requested path (relative to the API root, wp-json.php) + * + * @var string + */ + public $path = ''; + + /** + * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE) + * + * @var string + */ + public $method = 'HEAD'; + + /** + * Request parameters + * + * This acts as an abstraction of the superglobals + * (GET => $_GET, POST => $_POST) + * + * @var array + */ + public $params = array( 'GET' => array(), 'POST' => array() ); + + /** + * Request headers + * + * @var array + */ + public $headers = array(); + + /** + * Request files (matches $_FILES) + * + * @var array + */ + public $files = array(); + + /** + * Request/Response handler, either JSON by default + * or XML if requested by client + * + * @var WC_API_Handler + */ + public $handler; + + + /** + * Setup class and set request/response handler + * + * @since 2.1 + * @param $path + * @return WC_API_Server + */ + public function __construct( $path ) { + + if ( empty( $path ) ) { + if ( isset( $_SERVER['PATH_INFO'] ) ) { + $path = $_SERVER['PATH_INFO']; + } else { + $path = '/'; + } + } + + $this->path = $path; + $this->method = $_SERVER['REQUEST_METHOD']; + $this->params['GET'] = $_GET; + $this->params['POST'] = $_POST; + $this->headers = $this->get_headers( $_SERVER ); + $this->files = $_FILES; + + // Compatibility for clients that can't use PUT/PATCH/DELETE + if ( isset( $_GET['_method'] ) ) { + $this->method = strtoupper( $_GET['_method'] ); + } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { + $this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; + } + + // load response handler + $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this ); + + $this->handler = new $handler_class(); + } + + /** + * Check authentication for the request + * + * @since 2.1 + * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login + */ + public function check_authentication() { + + // allow plugins to remove default authentication or add their own authentication + $user = apply_filters( 'woocommerce_api_check_authentication', null, $this ); + + if ( is_a( $user, 'WP_User' ) ) { + + // API requests run under the context of the authenticated user + wp_set_current_user( $user->ID ); + + } elseif ( ! is_wp_error( $user ) ) { + + // WP_Errors are handled in serve_request() + $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) ); + + } + + return $user; + } + + /** + * Convert an error to an array + * + * This iterates over all error codes and messages to change it into a flat + * array. This enables simpler client behaviour, as it is represented as a + * list in JSON rather than an object/map + * + * @since 2.1 + * @param WP_Error $error + * @return array List of associative arrays with code and message keys + */ + protected function error_to_array( $error ) { + $errors = array(); + foreach ( (array) $error->errors as $code => $messages ) { + foreach ( (array) $messages as $message ) { + $errors[] = array( 'code' => $code, 'message' => $message ); + } + } + + return array( 'errors' => $errors ); + } + + /** + * Handle serving an API request + * + * Matches the current server URI to a route and runs the first matching + * callback then outputs a JSON representation of the returned value. + * + * @since 2.1 + * @uses WC_API_Server::dispatch() + */ + public function serve_request() { + + do_action( 'woocommerce_api_server_before_serve', $this ); + + $this->header( 'Content-Type', $this->handler->get_content_type(), true ); + + // the API is enabled by default + if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) { + + $this->send_status( 404 ); + + echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) ); + + return; + } + + $result = $this->check_authentication(); + + // if authorization check was successful, dispatch the request + if ( ! is_wp_error( $result ) ) { + $result = $this->dispatch(); + } + + // handle any dispatch errors + if ( is_wp_error( $result ) ) { + $data = $result->get_error_data(); + if ( is_array( $data ) && isset( $data['status'] ) ) { + $this->send_status( $data['status'] ); + } + + $result = $this->error_to_array( $result ); + } + + // This is a filter rather than an action, since this is designed to be + // re-entrant if needed + $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this ); + + if ( ! $served ) { + + if ( 'HEAD' === $this->method ) { + return; + } + + echo $this->handler->generate_response( $result ); + } + } + + /** + * Retrieve the route map + * + * The route map is an associative array with path regexes as the keys. The + * value is an indexed array with the callback function/method as the first + * item, and a bitmask of HTTP methods as the second item (see the class + * constants). + * + * Each route can be mapped to more than one callback by using an array of + * the indexed arrays. This allows mapping e.g. GET requests to one callback + * and POST requests to another. + * + * Note that the path regexes (array keys) must have @ escaped, as this is + * used as the delimiter with preg_match() + * + * @since 2.1 + * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` + */ + public function get_routes() { + + // index added by default + $endpoints = array( + + '/' => array( array( $this, 'get_index' ), self::READABLE ), + ); + + $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints ); + + // Normalise the endpoints + foreach ( $endpoints as $route => &$handlers ) { + if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) { + $handlers = array( $handlers ); + } + } + + return $endpoints; + } + + /** + * Match the request to a callback and call it + * + * @since 2.1 + * @return mixed The value returned by the callback, or a WP_Error instance + */ + public function dispatch() { + + switch ( $this->method ) { + + case 'HEAD' : + case 'GET' : + $method = self::METHOD_GET; + break; + + case 'POST' : + $method = self::METHOD_POST; + break; + + case 'PUT' : + $method = self::METHOD_PUT; + break; + + case 'PATCH' : + $method = self::METHOD_PATCH; + break; + + case 'DELETE' : + $method = self::METHOD_DELETE; + break; + + default : + return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) ); + } + + foreach ( $this->get_routes() as $route => $handlers ) { + foreach ( $handlers as $handler ) { + $callback = $handler[0]; + $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; + + if ( ! ( $supported & $method ) ) { + continue; + } + + $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args ); + + if ( ! $match ) { + continue; + } + + if ( ! is_callable( $callback ) ) { + return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $args = array_merge( $args, $this->params['GET'] ); + if ( $method & self::METHOD_POST ) { + $args = array_merge( $args, $this->params['POST'] ); + } + if ( $supported & self::ACCEPT_DATA ) { + $data = $this->handler->parse_body( $this->get_raw_data() ); + $args = array_merge( $args, array( 'data' => $data ) ); + } elseif ( $supported & self::ACCEPT_RAW_DATA ) { + $data = $this->get_raw_data(); + $args = array_merge( $args, array( 'data' => $data ) ); + } + + $args['_method'] = $method; + $args['_route'] = $route; + $args['_path'] = $this->path; + $args['_headers'] = $this->headers; + $args['_files'] = $this->files; + + $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback ); + + // Allow plugins to halt the request via this filter + if ( is_wp_error( $args ) ) { + return $args; + } + + $params = $this->sort_callback_params( $callback, $args ); + if ( is_wp_error( $params ) ) { + return $params; + } + + return call_user_func_array( $callback, $params ); + } + } + + return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) ); + } + + /** + * urldecode deep. + * + * @since 2.2 + * @param string|array $value Data to decode with urldecode. + * + * @return string|array Decoded data. + */ + protected function urldecode_deep( $value ) { + if ( is_array( $value ) ) { + return array_map( array( $this, 'urldecode_deep' ), $value ); + } else { + return urldecode( $value ); + } + } + + /** + * Sort parameters by order specified in method declaration + * + * Takes a callback and a list of available params, then filters and sorts + * by the parameters the method actually needs, using the Reflection API + * + * @since 2.2 + * + * @param callable|array $callback the endpoint callback + * @param array $provided the provided request parameters + * + * @return array|WP_Error + */ + protected function sort_callback_params( $callback, $provided ) { + if ( is_array( $callback ) ) { + $ref_func = new ReflectionMethod( $callback[0], $callback[1] ); + } else { + $ref_func = new ReflectionFunction( $callback ); + } + + $wanted = $ref_func->getParameters(); + $ordered_parameters = array(); + + foreach ( $wanted as $param ) { + if ( isset( $provided[ $param->getName() ] ) ) { + // We have this parameters in the list to choose from + if ( 'data' == $param->getName() ) { + $ordered_parameters[] = $provided[ $param->getName() ]; + continue; + } + + $ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] ); + } elseif ( $param->isDefaultValueAvailable() ) { + // We don't have this parameter, but it's optional + $ordered_parameters[] = $param->getDefaultValue(); + } else { + // We don't have this parameter and it wasn't optional, abort! + return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) ); + } + } + + return $ordered_parameters; + } + + /** + * Get the site index. + * + * This endpoint describes the capabilities of the site. + * + * @since 2.3 + * @return array Index entity + */ + public function get_index() { + + // General site data + $available = array( + 'store' => array( + 'name' => get_option( 'blogname' ), + 'description' => get_option( 'blogdescription' ), + 'URL' => get_option( 'siteurl' ), + 'wc_version' => WC()->version, + 'version' => WC_API::VERSION, + 'routes' => array(), + 'meta' => array( + 'timezone' => wc_timezone_string(), + 'currency' => get_woocommerce_currency(), + 'currency_format' => get_woocommerce_currency_symbol(), + 'currency_position' => get_option( 'woocommerce_currency_pos' ), + 'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ), + 'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ), + 'price_num_decimals' => wc_get_price_decimals(), + 'tax_included' => wc_prices_include_tax(), + 'weight_unit' => get_option( 'woocommerce_weight_unit' ), + 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), + 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || wc_site_is_https() ), + 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ), + 'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ), + 'links' => array( + 'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/', + ), + ), + ), + ); + + // Find the available routes + foreach ( $this->get_routes() as $route => $callbacks ) { + $data = array(); + + $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); + + foreach ( self::$method_map as $name => $bitmask ) { + foreach ( $callbacks as $callback ) { + // Skip to the next route if any callback is hidden + if ( $callback[1] & self::HIDDEN_ENDPOINT ) { + continue 3; + } + + if ( $callback[1] & $bitmask ) { + $data['supports'][] = $name; + } + + if ( $callback[1] & self::ACCEPT_DATA ) { + $data['accepts_data'] = true; + } + + // For non-variable routes, generate links + if ( strpos( $route, '<' ) === false ) { + $data['meta'] = array( + 'self' => get_woocommerce_api_url( $route ), + ); + } + } + } + + $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data ); + } + + return apply_filters( 'woocommerce_api_index', $available ); + } + + /** + * Send a HTTP status code + * + * @since 2.1 + * @param int $code HTTP status + */ + public function send_status( $code ) { + status_header( $code ); + } + + /** + * Send a HTTP header + * + * @since 2.1 + * @param string $key Header key + * @param string $value Header value + * @param boolean $replace Should we replace the existing header? + */ + public function header( $key, $value, $replace = true ) { + header( sprintf( '%s: %s', $key, $value ), $replace ); + } + + /** + * Send a Link header + * + * @internal The $rel parameter is first, as this looks nicer when sending multiple + * + * @link http://tools.ietf.org/html/rfc5988 + * @link http://www.iana.org/assignments/link-relations/link-relations.xml + * + * @since 2.1 + * @param string $rel Link relation. Either a registered type, or an absolute URL + * @param string $link Target IRI for the link + * @param array $other Other parameters to send, as an associative array + */ + public function link_header( $rel, $link, $other = array() ) { + + $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) ); + + foreach ( $other as $key => $value ) { + + if ( 'title' == $key ) { + + $value = '"' . $value . '"'; + } + + $header .= '; ' . $key . '=' . $value; + } + + $this->header( 'Link', $header, false ); + } + + /** + * Send pagination headers for resources + * + * @since 2.1 + * @param WP_Query|WP_User_Query|stdClass $query + */ + public function add_pagination_headers( $query ) { + + // WP_User_Query + if ( is_a( $query, 'WP_User_Query' ) ) { + + $single = count( $query->get_results() ) == 1; + $total = $query->get_total(); + + if ( $query->get( 'number' ) > 0 ) { + $page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1; + $total_pages = ceil( $total / $query->get( 'number' ) ); + } else { + $page = 1; + $total_pages = 1; + } + } elseif ( is_a( $query, 'stdClass' ) ) { + $page = $query->page; + $single = $query->is_single; + $total = $query->total; + $total_pages = $query->total_pages; + + // WP_Query + } else { + + $page = $query->get( 'paged' ); + $single = $query->is_single(); + $total = $query->found_posts; + $total_pages = $query->max_num_pages; + } + + if ( ! $page ) { + $page = 1; + } + + $next_page = absint( $page ) + 1; + + if ( ! $single ) { + + // first/prev + if ( $page > 1 ) { + $this->link_header( 'first', $this->get_paginated_url( 1 ) ); + $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) ); + } + + // next + if ( $next_page <= $total_pages ) { + $this->link_header( 'next', $this->get_paginated_url( $next_page ) ); + } + + // last + if ( $page != $total_pages ) { + $this->link_header( 'last', $this->get_paginated_url( $total_pages ) ); + } + } + + $this->header( 'X-WC-Total', $total ); + $this->header( 'X-WC-TotalPages', $total_pages ); + + do_action( 'woocommerce_api_pagination_headers', $this, $query ); + } + + /** + * Returns the request URL with the page query parameter set to the specified page + * + * @since 2.1 + * @param int $page + * @return string + */ + private function get_paginated_url( $page ) { + + // remove existing page query param + $request = remove_query_arg( 'page' ); + + // add provided page query param + $request = urldecode( add_query_arg( 'page', $page, $request ) ); + + // get the home host + $host = parse_url( get_home_url(), PHP_URL_HOST ); + + return set_url_scheme( "http://{$host}{$request}" ); + } + + /** + * Retrieve the raw request entity (body) + * + * @since 2.1 + * @return string + */ + public function get_raw_data() { + // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6 + if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { + return file_get_contents( 'php://input' ); + } + + global $HTTP_RAW_POST_DATA; + + // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, + // but we can do it ourself. + if ( ! isset( $HTTP_RAW_POST_DATA ) ) { + $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); + } + + return $HTTP_RAW_POST_DATA; + } + + /** + * Parse an RFC3339 datetime into a MySQl datetime + * + * Invalid dates default to unix epoch + * + * @since 2.1 + * @param string $datetime RFC3339 datetime + * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) + */ + public function parse_datetime( $datetime ) { + + // Strip millisecond precision (a full stop followed by one or more digits) + if ( strpos( $datetime, '.' ) !== false ) { + $datetime = preg_replace( '/\.\d+/', '', $datetime ); + } + + // default timezone to UTC + $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); + + try { + + $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); + + } catch ( Exception $e ) { + + $datetime = new DateTime( '@0' ); + + } + + return $datetime->format( 'Y-m-d H:i:s' ); + } + + /** + * Format a unix timestamp or MySQL datetime into an RFC3339 datetime + * + * @since 2.1 + * @param int|string $timestamp unix timestamp or MySQL datetime + * @param bool $convert_to_utc + * @param bool $convert_to_gmt Use GMT timezone. + * @return string RFC3339 datetime + */ + public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) { + if ( $convert_to_gmt ) { + if ( is_numeric( $timestamp ) ) { + $timestamp = date( 'Y-m-d H:i:s', $timestamp ); + } + + $timestamp = get_gmt_from_date( $timestamp ); + } + + if ( $convert_to_utc ) { + $timezone = new DateTimeZone( wc_timezone_string() ); + } else { + $timezone = new DateTimeZone( 'UTC' ); + } + + try { + + if ( is_numeric( $timestamp ) ) { + $date = new DateTime( "@{$timestamp}" ); + } else { + $date = new DateTime( $timestamp, $timezone ); + } + + // convert to UTC by adjusting the time based on the offset of the site's timezone + if ( $convert_to_utc ) { + $date->modify( -1 * $date->getOffset() . ' seconds' ); + } + } catch ( Exception $e ) { + + $date = new DateTime( '@0' ); + } + + return $date->format( 'Y-m-d\TH:i:s\Z' ); + } + + /** + * Extract headers from a PHP-style $_SERVER array + * + * @since 2.1 + * @param array $server Associative array similar to $_SERVER + * @return array Headers extracted from the input + */ + public function get_headers( $server ) { + $headers = array(); + // CONTENT_* headers are not prefixed with HTTP_ + $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true ); + + foreach ( $server as $key => $value ) { + if ( strpos( $key, 'HTTP_' ) === 0 ) { + $headers[ substr( $key, 5 ) ] = $value; + } elseif ( isset( $additional[ $key ] ) ) { + $headers[ $key ] = $value; + } + } + + return $headers; + } +} diff --git a/includes/api/legacy/v3/class-wc-api-taxes.php b/includes/api/legacy/v3/class-wc-api-taxes.php new file mode 100644 index 00000000000..2fbd6184109 --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-taxes.php @@ -0,0 +1,691 @@ + + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /taxes + $routes[ $this->base ] = array( + array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /taxes/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_taxes_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /taxes/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_tax' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_tax' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), + array( array( $this, 'delete_tax' ), WC_API_SERVER::DELETABLE ), + ); + + # GET/POST /taxes/classes + $routes[ $this->base . '/classes' ] = array( + array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /taxes/classes/count + $routes[ $this->base . '/classes/count' ] = array( + array( array( $this, 'get_tax_classes_count' ), WC_API_Server::READABLE ), + ); + + # GET /taxes/classes/ + $routes[ $this->base . '/classes/(?P\w[\w\s\-]*)' ] = array( + array( array( $this, 'delete_tax_class' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /taxes/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all taxes + * + * @since 2.5.0 + * + * @param string $fields + * @param array $filter + * @param string $class + * @param int $page + * + * @return array + */ + public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) { + if ( ! empty( $class ) ) { + $filter['tax_rate_class'] = $class; + } + + $filter['page'] = $page; + + $query = $this->query_tax_rates( $filter ); + + $taxes = array(); + + foreach ( $query['results'] as $tax ) { + $taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) ); + } + + // Set pagination headers + $this->server->add_pagination_headers( $query['headers'] ); + + return array( 'taxes' => $taxes ); + } + + /** + * Get the tax for the given ID + * + * @since 2.5.0 + * + * @param int $id The tax ID + * @param string $fields fields to include in response + * + * @return array|WP_Error + */ + public function get_tax( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Permissions check + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 ); + } + + // Get tax rate details + $tax = WC_Tax::_get_tax_rate( $id ); + + if ( is_wp_error( $tax ) || empty( $tax ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $tax_data = array( + 'id' => (int) $tax['tax_rate_id'], + 'country' => $tax['tax_rate_country'], + 'state' => $tax['tax_rate_state'], + 'postcode' => '', + 'city' => '', + 'rate' => $tax['tax_rate'], + 'name' => $tax['tax_rate_name'], + 'priority' => (int) $tax['tax_rate_priority'], + 'compound' => (bool) $tax['tax_rate_compound'], + 'shipping' => (bool) $tax['tax_rate_shipping'], + 'order' => (int) $tax['tax_rate_order'], + 'class' => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard', + ); + + // Get locales from a tax rate + $locales = $wpdb->get_results( $wpdb->prepare( " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", $id ) ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $tax_data[ $locale->location_type ] = $locale->location_code; + } + } + + return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a tax + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_tax( $data ) { + try { + if ( ! isset( $data['tax'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this ); + + $tax_data = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '', + 'tax_rate_name' => '', + 'tax_rate_priority' => 1, + 'tax_rate_compound' => 0, + 'tax_rate_shipping' => 1, + 'tax_rate_order' => 0, + 'tax_rate_class' => '', + ); + + foreach ( $tax_data as $key => $value ) { + $new_key = str_replace( 'tax_rate_', '', $key ); + $new_key = 'tax_rate' === $new_key ? 'rate' : $new_key; + + if ( isset( $data[ $new_key ] ) ) { + if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) { + $tax_data[ $key ] = $data[ $new_key ] ? 1 : 0; + } else { + $tax_data[ $key ] = $data[ $new_key ]; + } + } + } + + // Create tax rate + $id = WC_Tax::_insert_tax_rate( $tax_data ); + + // Add locales + if ( ! empty( $data['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); + } + + if ( ! empty( $data['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); + } + + do_action( 'woocommerce_api_create_tax', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_tax( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a tax + * + * @since 2.5.0 + * + * @param int $id The tax ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_tax( $id, $data ) { + try { + if ( ! isset( $data['tax'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 ); + } + + $data = $data['tax']; + + // Get current tax rate data + $tax = $this->get_tax( $id ); + + if ( is_wp_error( $tax ) ) { + $error_data = $tax->get_error_data(); + throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] ); + } + + $current_data = $tax['tax']; + $data = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this ); + $tax_data = array(); + $default_fields = array( + 'tax_rate_country', + 'tax_rate_state', + 'tax_rate', + 'tax_rate_name', + 'tax_rate_priority', + 'tax_rate_compound', + 'tax_rate_shipping', + 'tax_rate_order', + 'tax_rate_class', + ); + + foreach ( $data as $key => $value ) { + $new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key; + + // Check if the key is valid + if ( ! in_array( $new_key, $default_fields ) ) { + continue; + } + + // Test new data against current data + if ( $value === $current_data[ $key ] ) { + continue; + } + + // Fix compund and shipping values + if ( in_array( $key, array( 'compound', 'shipping' ) ) ) { + $value = $value ? 1 : 0; + } + + $tax_data[ $new_key ] = $value; + } + + // Update tax rate + WC_Tax::_update_tax_rate( $id, $tax_data ); + + // Update locales + if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); + } + + if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); + } + + do_action( 'woocommerce_api_edit_tax_rate', $id, $data ); + + return $this->get_tax( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a tax + * + * @since 2.5.0 + * + * @param int $id The tax ID + * + * @return array|WP_Error + */ + public function delete_tax( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 ); + } + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of taxes + * + * @since 2.5.0 + * + * @param string $class + * @param array $filter + * + * @return array|WP_Error + */ + public function get_taxes_count( $class = null, $filter = array() ) { + try { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $class ) ) { + $filter['tax_rate_class'] = $class; + } + + $query = $this->query_tax_rates( $filter, true ); + + return array( 'count' => (int) $query['headers']->total ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Helper method to get tax rates objects + * + * @since 2.5.0 + * + * @param array $args + * @param bool $count_only + * + * @return array + */ + protected function query_tax_rates( $args, $count_only = false ) { + global $wpdb; + + $results = ''; + + // Set args + $args = $this->merge_query_args( $args, array() ); + + $query = " + SELECT tax_rate_id + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE 1 = 1 + "; + + // Filter by tax class + if ( ! empty( $args['tax_rate_class'] ) ) { + $tax_rate_class = 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : ''; + $query .= " AND tax_rate_class = '$tax_rate_class'"; + } + + // Order tax rates + $order_by = ' ORDER BY tax_rate_order'; + + // Pagination + $per_page = isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' ); + $offset = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0; + $pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page ); + + if ( ! $count_only ) { + $results = $wpdb->get_results( $query . $order_by . $pagination ); + } + + $wpdb->get_results( $query ); + $headers = new stdClass; + $headers->page = $args['paged']; + $headers->total = (int) $wpdb->num_rows; + $headers->is_single = $per_page > $headers->total; + $headers->total_pages = ceil( $headers->total / $per_page ); + + return array( + 'results' => $results, + 'headers' => $headers, + ); + } + + /** + * Bulk update or insert taxes + * Accepts an array with taxes in the formats supported by + * WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax() + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + try { + if ( ! isset( $data['taxes'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 ); + } + + $data = $data['taxes']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $taxes = array(); + + foreach ( $data as $_tax ) { + $tax_id = 0; + + // Try to get the tax rate ID + if ( isset( $_tax['id'] ) ) { + $tax_id = intval( $_tax['id'] ); + } + + if ( $tax_id ) { + + // Tax rate exists / edit tax rate + $edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) ); + + if ( is_wp_error( $edit ) ) { + $taxes[] = array( + 'id' => $tax_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $taxes[] = $edit['tax']; + } + } else { + + // Tax rate don't exists / create tax rate + $new = $this->create_tax( array( 'tax' => $_tax ) ); + + if ( is_wp_error( $new ) ) { + $taxes[] = array( + 'id' => $tax_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $taxes[] = $new['tax']; + } + } + } + + return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get all tax classes + * + * @since 2.5.0 + * + * @param string $fields + * + * @return array|WP_Error + */ + public function get_tax_classes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 ); + } + + $tax_classes = array(); + + // Add standard class + $tax_classes[] = array( + 'slug' => 'standard', + 'name' => __( 'Standard rate', 'woocommerce' ), + ); + + $classes = WC_Tax::get_tax_classes(); + + foreach ( $classes as $class ) { + $tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array( + 'slug' => sanitize_title( $class ), + 'name' => $class, + ), $class, $fields, $this ); + } + + return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a tax class. + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_tax_class( $data ) { + try { + if ( ! isset( $data['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 ); + } + + $data = $data['tax_class']; + + if ( empty( $data['name'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + $name = sanitize_text_field( $data['name'] ); + $slug = sanitize_title( $name ); + $classes = WC_Tax::get_tax_classes(); + $exists = false; + + // Check if class exists. + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $slug ) { + $exists = true; + break; + } + } + + // Return error if tax class already exists. + if ( $exists ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_tax_class', __( 'Tax class already exists', 'woocommerce' ), 401 ); + } + + // Add the new class. + $classes[] = $name; + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + do_action( 'woocommerce_api_create_tax_class', $slug, $data ); + + $this->server->send_status( 201 ); + + return array( + 'tax_class' => array( + 'slug' => $slug, + 'name' => $name, + ), + ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a tax class + * + * @since 2.5.0 + * + * @param int $slug The tax class slug + * + * @return array|WP_Error + */ + public function delete_tax_class( $slug ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 ); + } + + $slug = sanitize_title( $slug ); + $classes = WC_Tax::get_tax_classes(); + $deleted = false; + + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $slug ) { + unset( $classes[ $key ] ); + $deleted = true; + break; + } + } + + if ( ! $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 ); + } + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + // Delete tax rate locations locations from the selected class. + $wpdb->query( $wpdb->prepare( " + DELETE locations.* + FROM {$wpdb->prefix}woocommerce_tax_rate_locations AS locations + INNER JOIN + {$wpdb->prefix}woocommerce_tax_rates AS rates + ON rates.tax_rate_id = locations.tax_rate_id + WHERE rates.tax_rate_class = '%s' + ", $slug ) ); + + // Delete tax rates in the selected class. + $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_class' => $slug ), array( '%s' ) ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of tax classes + * + * @since 2.5.0 + * + * @return array|WP_Error + */ + public function get_tax_classes_count() { + try { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 ); + } + + $total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate + + return array( 'count' => $total ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v3/class-wc-api-webhooks.php b/includes/api/legacy/v3/class-wc-api-webhooks.php new file mode 100644 index 00000000000..700aa3ea0c0 --- /dev/null +++ b/includes/api/legacy/v3/class-wc-api-webhooks.php @@ -0,0 +1,480 @@ +base ] = array( + array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ), + array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /webhooks/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /webhooks/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ), + ); + + # GET /webhooks//deliveries + $routes[ $this->base . '/(?P\d+)/deliveries' ] = array( + array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ), + ); + + # GET /webhooks//deliveries/ + $routes[ $this->base . '/(?P\d+)/deliveries/(?P\d+)' ] = array( + array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ), + ); + + return $routes; + } + + /** + * Get all webhooks + * + * @since 2.2 + * + * @param array $fields + * @param array $filter + * @param string $status + * @param int $page + * + * @return array + */ + public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_webhooks( $filter ); + + $webhooks = array(); + + foreach ( $query->posts as $webhook_id ) { + + if ( ! $this->is_readable( $webhook_id ) ) { + continue; + } + + $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'webhooks' => $webhooks ); + } + + /** + * Get the webhook for the given ID + * + * @since 2.2 + * @param int $id webhook ID + * @param array $fields + * @return array|WP_Error + */ + public function get_webhook( $id, $fields = null ) { + + // ensure webhook ID is valid & user has permission to read + $id = $this->validate_request( $id, 'shop_webhook', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $webhook = new WC_Webhook( $id ); + + $webhook_data = array( + 'id' => $webhook->id, + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'created_at' => $this->server->format_datetime( $webhook->get_post_data()->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $webhook->get_post_data()->post_modified_gmt ), + ); + + return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) ); + } + + /** + * Get the total number of webhooks + * + * @since 2.2 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_webhooks_count( $status = null, $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_shop_webhooks' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $query = $this->query_webhooks( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create an webhook + * + * @since 2.2 + * + * @param array $data parsed webhook data + * + * @return array|WP_Error + */ + public function create_webhook( $data ) { + + try { + if ( ! isset( $data['webhook'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 ); + } + + $data = $data['webhook']; + + // permission check + if ( ! current_user_can( 'publish_shop_webhooks' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this ); + + // validate topic + if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 ); + } + + // validate delivery URL + if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 ); + } + + $webhook_data = apply_filters( 'woocommerce_new_webhook_data', array( + 'post_type' => 'shop_webhook', + 'post_status' => 'publish', + 'ping_status' => 'closed', + 'post_author' => get_current_user_id(), + 'post_password' => strlen( ( $password = uniqid( 'webhook_' ) ) ) > 20 ? substr( $password, 0, 20 ) : $password, + // @codingStandardsIgnoreStart + 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ), + // @codingStandardsIgnoreEnd + ), $data, $this ); + + $webhook_id = wp_insert_post( $webhook_data ); + + if ( is_wp_error( $webhook_id ) || ! $webhook_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_webhook', sprintf( __( 'Cannot create webhook: %s', 'woocommerce' ), is_wp_error( $webhook_id ) ? implode( ', ', $webhook_id->get_error_messages() ) : '0' ), 500 ); + } + + $webhook = new WC_Webhook( $webhook_id ); + + // set topic, delivery URL, and optional secret + $webhook->set_topic( $data['topic'] ); + $webhook->set_delivery_url( $data['delivery_url'] ); + + // set secret if provided, defaults to API users consumer secret + $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : '' ); + + // Set API version to legacy v3. + $webhook->set_api_version( 'legacy_v3' ); + + // send ping + $webhook->deliver_ping(); + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_webhook', $webhook->id, $this ); + + delete_transient( 'woocommerce_webhook_ids' ); + + return $this->get_webhook( $webhook->id ); + + } catch ( WC_API_Exception $e ) { + + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a webhook + * + * @since 2.2 + * + * @param int $id webhook ID + * @param array $data parsed webhook data + * + * @return array|WP_Error + */ + public function edit_webhook( $id, $data ) { + + try { + if ( ! isset( $data['webhook'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 ); + } + + $data = $data['webhook']; + + $id = $this->validate_request( $id, 'shop_webhook', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this ); + + $webhook = new WC_Webhook( $id ); + + // update topic + if ( ! empty( $data['topic'] ) ) { + + if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) { + + $webhook->set_topic( $data['topic'] ); + + } else { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 ); + } + } + + // update delivery URL + if ( ! empty( $data['delivery_url'] ) ) { + if ( wc_is_valid_url( $data['delivery_url'] ) ) { + + $webhook->set_delivery_url( $data['delivery_url'] ); + + } else { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 ); + } + } + + // update secret + if ( ! empty( $data['secret'] ) ) { + $webhook->set_secret( $data['secret'] ); + } + + // update status + if ( ! empty( $data['status'] ) ) { + $webhook->update_status( $data['status'] ); + } + + // update user ID + $webhook_data = array( + 'ID' => $webhook->id, + 'post_author' => get_current_user_id(), + ); + + // update name + if ( ! empty( $data['name'] ) ) { + $webhook_data['post_title'] = $data['name']; + } + + // update post + wp_update_post( $webhook_data ); + + do_action( 'woocommerce_api_edit_webhook', $webhook->id, $this ); + + delete_transient( 'woocommerce_webhook_ids' ); + + return $this->get_webhook( $id ); + + } catch ( WC_API_Exception $e ) { + + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a webhook + * + * @since 2.2 + * @param int $id webhook ID + * @return array|WP_Error + */ + public function delete_webhook( $id ) { + + $id = $this->validate_request( $id, 'shop_webhook', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + do_action( 'woocommerce_api_delete_webhook', $id, $this ); + + delete_transient( 'woocommerce_webhook_ids' ); + + // no way to manage trashed webhooks at the moment, so force delete + return $this->delete( $id, 'webhook', true ); + } + + /** + * Helper method to get webhook post objects + * + * @since 2.2 + * @param array $args request arguments for filtering query. + * @return WP_Query + */ + private function query_webhooks( $args ) { + + // Set base query arguments. + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'shop_webhook', + ); + + // Add status argument. + if ( ! empty( $args['status'] ) ) { + switch ( $args['status'] ) { + case 'active' : + $query_args['post_status'] = 'publish'; + break; + case 'paused' : + $query_args['post_status'] = 'draft'; + break; + case 'disabled' : + $query_args['post_status'] = 'pending'; + break; + case 'all' : + $query_args['post_status'] = 'any'; + break; + default : + $query_args['post_status'] = 'publish'; + break; + } + unset( $args['status'] ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Get deliveries for a webhook + * + * @since 2.2 + * @param string $webhook_id webhook ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_webhook_deliveries( $webhook_id, $fields = null ) { + + // Ensure ID is valid webhook ID + $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' ); + + if ( is_wp_error( $webhook_id ) ) { + return $webhook_id; + } + + $webhook = new WC_Webhook( $webhook_id ); + $logs = $webhook->get_delivery_logs(); + $delivery_logs = array(); + + foreach ( $logs as $log ) { + + // Add timestamp + $log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt ); + + // Remove comment object + unset( $log['comment'] ); + + $delivery_logs[] = $log; + } + + return array( 'webhook_deliveries' => $delivery_logs ); + } + + /** + * Get the delivery log for the given webhook ID and delivery ID + * + * @since 2.2 + * + * @param string $webhook_id webhook ID + * @param string $id delivery log ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_webhook_delivery( $webhook_id, $id, $fields = null ) { + try { + // Validate webhook ID + $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' ); + + if ( is_wp_error( $webhook_id ) ) { + return $webhook_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 ); + } + + $webhook = new WC_Webhook( $webhook_id ); + + $log = $webhook->get_delivery_log( $id ); + + if ( ! $log ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 ); + } + + $delivery_log = $log; + + // Add timestamp + $delivery_log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt ); + + // Remove comment object + unset( $delivery_log['comment'] ); + + return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', $delivery_log, $id, $fields, $log, $webhook_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/v3/interface-wc-api-handler.php b/includes/api/legacy/v3/interface-wc-api-handler.php new file mode 100644 index 00000000000..484f9f57f02 --- /dev/null +++ b/includes/api/legacy/v3/interface-wc-api-handler.php @@ -0,0 +1,47 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for coupons. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Query args. + * + * @param array $args Query args + * @param WP_REST_Request $request Request data. + * @return array + */ + public function query_args( $args, $request ) { + if ( ! empty( $request['code'] ) ) { + $id = wc_get_coupon_id_by_code( $request['code'] ); + $args['post__in'] = array( $id ); + } + + return $args; + } + + /** + * Prepare a single coupon output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + $coupon = new WC_Coupon( (int) $post->ID ); + $_data = $coupon->get_data(); + + $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); + $format_date = array( 'date_created', 'date_modified' ); + $format_date_utc = array( 'date_expires' ); + $format_null = array( 'usage_limit', 'usage_limit_per_user' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $_data[ $key ] = wc_format_decimal( $_data[ $key ], 2 ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ], false ) : null; + } + foreach ( $format_date_utc as $key ) { + $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; + } + + // Format null values. + foreach ( $format_null as $key ) { + $_data[ $key ] = $_data[ $key ] ? $_data[ $key ] : null; + } + + $data = array( + 'id' => $_data['id'], + 'code' => $_data['code'], + 'date_created' => $_data['date_created'], + 'date_modified' => $_data['date_modified'], + 'discount_type' => $_data['discount_type'], + 'description' => $_data['description'], + 'amount' => $_data['amount'], + 'expiry_date' => $_data['date_expires'], + 'usage_count' => $_data['usage_count'], + 'individual_use' => $_data['individual_use'], + 'product_ids' => $_data['product_ids'], + 'exclude_product_ids' => $_data['excluded_product_ids'], + 'usage_limit' => $_data['usage_limit'], + 'usage_limit_per_user' => $_data['usage_limit_per_user'], + 'limit_usage_to_x_items' => $_data['limit_usage_to_x_items'], + 'free_shipping' => $_data['free_shipping'], + 'product_categories' => $_data['product_categories'], + 'excluded_product_categories' => $_data['excluded_product_categories'], + 'exclude_sale_items' => $_data['exclude_sale_items'], + 'minimum_amount' => $_data['minimum_amount'], + 'maximum_amount' => $_data['maximum_amount'], + 'email_restrictions' => $_data['email_restrictions'], + 'used_by' => $_data['used_by'], + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $post, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Only reutrn writeable props from schema. + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single coupon for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $coupon = new WC_Coupon( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Update to schema to make compatible with CRUD schema. + if ( $request['exclude_product_ids'] ) { + $request['excluded_product_ids'] = $request['exclude_product_ids']; + } + if ( $request['expiry_date'] ) { + $request['date_expires'] = $request['expiry_date']; + } + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) { + if ( empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + } + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'code' : + $coupon_code = wc_format_coupon_code( $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon->set_code( $coupon_code ); + break; + case 'description' : + $coupon->set_description( wp_filter_post_kses( $value ) ); + break; + case 'expiry_date' : + $coupon->set_date_expires( $value ); + break; + default : + if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { + $coupon->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param WC_Coupon $coupon The coupon object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request ); + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $coupon_id = $this->save_coupon( $request ); + if ( is_wp_error( $coupon_id ) ) { + return $coupon_id; + } + + $post = get_post( $coupon_id ); + $this->update_additional_fields_for_object( $post, $request ); + + $this->add_post_meta_fields( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Update a single coupon. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $post_id = (int) $request['id']; + + if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon_id = $this->save_coupon( $request ); + if ( is_wp_error( $coupon_id ) ) { + return $coupon_id; + } + + $post = get_post( $coupon_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Saves a coupon to the database. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|int + */ + protected function save_coupon( $request ) { + try { + $coupon = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $coupon ) ) { + return $coupon; + } + + $coupon->save(); + return $coupon->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the Coupon's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'fixed_cart', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'amount' => array( + 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'expiry_date' => array( + 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'individual_use' => array( + 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( "List of product IDs the coupon can be used on.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_product_ids' => array( + 'description' => __( "List of product IDs the coupon cannot be used on.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'limit_usage_to_x_items' => array( + 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'free_shipping' => array( + 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_categories' => array( + 'description' => __( "List of category IDs the coupon applies to.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_categories' => array( + 'description' => __( "List of category IDs the coupon does not apply to.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email_restrictions' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + 'used_by' => array( + 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['code'] = array( + 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-customer-downloads-controller.php b/includes/api/v1/class-wc-rest-customer-downloads-controller.php new file mode 100644 index 00000000000..7557161adef --- /dev/null +++ b/includes/api/v1/class-wc-rest-customer-downloads-controller.php @@ -0,0 +1,252 @@ +/downloads endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Customers controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Controller + */ +class WC_REST_Customer_Downloads_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'customers/(?P[\d]+)/downloads'; + + /** + * Register the routes for customers. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'customer_id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $customer = get_user_by( 'id', (int) $request['customer_id'] ); + + if ( ! $customer ) { + return new WP_Error( 'woocommerce_rest_customer_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_user_permissions( 'read', $customer->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all customer downloads. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $downloads = wc_get_customer_available_downloads( (int) $request['customer_id'] ); + + $data = array(); + foreach ( $downloads as $download_data ) { + $download = $this->prepare_item_for_response( (object) $download_data, $request ); + $download = $this->prepare_response_for_collection( $download ); + $data[] = $download; + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a single download output for response. + * + * @param stdObject $download Download object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $download, $request ) { + $data = (array) $download; + $data['access_expires'] = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never'; + $data['downloads_remaining'] = '' === $data['downloads_remaining'] ? 'unlimited' : $data['downloads_remaining']; + + // Remove "product_name" since it's new in 3.0. + unset( $data['product_name'] ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $download, $request ) ); + + /** + * Filter customer download data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdObject $download Download object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $download Download object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given customer download. + */ + protected function prepare_links( $download, $request ) { + $base = str_replace( '(?P[\d]+)', $request['customer_id'], $this->rest_base ); + $links = array( + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'product' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $download->product_id ) ), + ), + 'order' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $download->order_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Customer Download's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer_download', + 'type' => 'object', + 'properties' => array( + 'download_url' => array( + 'description' => __( 'Download file URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_id' => array( + 'description' => __( 'Download ID (MD5).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Downloadable product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_name' => array( + 'description' => __( 'Downloadable file name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_id' => array( + 'description' => __( 'Order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'downloads_remaining' => array( + 'description' => __( 'Number of downloads remaining.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires' => array( + 'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File details.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-customers-controller.php b/includes/api/v1/class-wc-rest-customers-controller.php new file mode 100644 index 00000000000..0e46e6df95c --- /dev/null +++ b/includes/api/v1/class-wc-rest-customers-controller.php @@ -0,0 +1,926 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'New user email address.', 'woocommerce' ), + ), + 'username' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), + 'description' => __( 'New user username.', 'woocommerce' ), + 'type' => 'string', + ), + 'password' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), + 'description' => __( 'New user password.', 'woocommerce' ), + 'type' => 'string', + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array( + 'default' => 0, + 'type' => 'integer', + 'description' => __( 'ID to reassign posts to.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create customers. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $prepared_args = array(); + $prepared_args['exclude'] = $request['exclude']; + $prepared_args['include'] = $request['include']; + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'ID', + 'include' => 'include', + 'name' => 'display_name', + 'registered_date' => 'registered', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['search'] = $request['search']; + + if ( '' !== $prepared_args['search'] ) { + $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; + } + + // Filter by email. + if ( ! empty( $request['email'] ) ) { + $prepared_args['search'] = $request['email']; + $prepared_args['search_columns'] = array( 'user_email' ); + } + + // Filter by role. + if ( 'all' !== $request['role'] ) { + $prepared_args['role'] = $request['role']; + } + + /** + * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. + * + * @see https://developer.wordpress.org/reference/classes/wp_user_query/ + * + * @param array $prepared_args Array of arguments for WP_User_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); + + $query = new WP_User_Query( $prepared_args ); + + $users = array(); + foreach ( $query->results as $user ) { + $data = $this->prepare_item_for_response( $user, $request ); + $users[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $users ); + + // Store pagation values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $prepared_args['fields'] = 'ID'; + + $total_users = $query->get_total(); + if ( $total_users < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'] ); + unset( $prepared_args['offset'] ); + $count_query = new WP_User_Query( $prepared_args ); + $total_users = $count_query->get_total(); + } + $response->header( 'X-WP-Total', (int) $total_users ); + $max_pages = ceil( $total_users / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + try { + if ( ! empty( $request['id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 ); + } + + // Sets the username. + $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; + + // Sets the password. + $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; + + // Create customer. + $customer = new WC_Customer; + $customer->set_username( $request['username'] ); + $customer->set_password( $request['password'] ); + $customer->set_email( $request['email'] ); + $customer->save(); + + if ( ! $customer->get_id() ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); + } + + $this->update_customer_meta_fields( $customer, $request ); + $customer->save(); + + $user_data = get_userdata( $customer->get_id() ); + $this->update_additional_fields_for_object( $user_data, $request ); + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $user_data Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) ); + + return $response; + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $user_data = get_userdata( $id ); + + if ( empty( $id ) || empty( $user_data->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $customer = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $customer ); + + return $response; + } + + /** + * Update a single user. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $id = (int) $request['id']; + $customer = new WC_Customer( $id ); + + if ( ! $customer->get_id() ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 ); + } + + // Customer email. + if ( isset( $request['email'] ) ) { + $customer->set_email( sanitize_email( $request['email'] ) ); + } + + // Customer password. + if ( isset( $request['password'] ) ) { + $customer->set_password( wc_clean( $request['password'] ) ); + } + + $this->update_customer_meta_fields( $customer, $request ); + $customer->save(); + + $user_data = get_userdata( $customer->get_id() ); + $this->update_additional_fields_for_object( $user_data, $request ); + + if ( ! is_user_member_of_blog( $user_data->ID ) ) { + $user_data->add_role( 'customer' ); + } + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $customer Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $response ); + return $response; + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $user_data = get_userdata( $id ); + if ( ! $user_data ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $reassign ) ) { + if ( $reassign === $id || ! get_userdata( $reassign ) ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + + /** Include admin customer functions to get access to wp_delete_user() */ + require_once ABSPATH . 'wp-admin/includes/user.php'; + + $customer = new WC_Customer( $id ); + + if ( ! is_null( $reassign ) ) { + $result = $customer->delete_and_reassign( $reassign ); + } else { + $result = $customer->delete(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a customer is deleted via the REST API. + * + * @param WP_User $user_data User data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request ); + + return $response; + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $user_data User object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $user_data, $request ) { + $customer = new WC_Customer( $user_data->ID ); + $_data = $customer->get_data(); + $last_order = wc_get_customer_last_order( $customer->get_id() ); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; // v1 API used UTC. + } + + $data = array( + 'id' => $_data['id'], + 'date_created' => $_data['date_created'], + 'date_modified' => $_data['date_modified'], + 'email' => $_data['email'], + 'first_name' => $_data['first_name'], + 'last_name' => $_data['last_name'], + 'username' => $_data['username'], + 'last_order' => array( + 'id' => is_object( $last_order ) ? $last_order->get_id() : null, + 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->get_date_created() ) : null, // v1 API used UTC. + ), + 'orders_count' => $customer->get_order_count(), + 'total_spent' => $customer->get_total_spent(), + 'avatar_url' => $customer->get_avatar_url(), + 'billing' => $_data['billing'], + 'shipping' => $_data['shipping'], + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $user_data ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $user_data User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); + } + + /** + * Update customer meta fields. + * + * @param WC_Customer $customer + * @param WP_REST_Request $request + */ + protected function update_customer_meta_fields( $customer, $request ) { + $schema = $this->get_item_schema(); + + // Customer first name. + if ( isset( $request['first_name'] ) ) { + $customer->set_first_name( wc_clean( $request['first_name'] ) ); + } + + // Customer last name. + if ( isset( $request['last_name'] ) ) { + $customer->set_last_name( wc_clean( $request['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $request['billing'] ) ) { + foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) { + if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) { + $customer->{"set_billing_{$field}"}( $request['billing'][ $field ] ); + } + } + } + + // Customer shipping address. + if ( isset( $request['shipping'] ) ) { + foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) { + if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { + $customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ] ); + } + } + } + } + + /** + * Prepare links for the request. + * + * @param WP_User $customer Customer object. + * @return array Links for the given customer. + */ + protected function prepare_links( $customer ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'last_order' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Last order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date' => array( + 'description' => __( 'The date of the customer last order, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'orders_count' => array( + 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_spent' => array( + 'description' => __( 'Total amount spent.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get role names. + * + * @return array + */ + protected function get_role_names() { + global $wp_roles; + + return array_keys( $wp_roles->role_names ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'name', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'include', + 'name', + 'registered_date', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['email'] = array( + 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['role'] = array( + 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'customer', + 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), + 'validate_callback' => 'rest_validate_request_arg', + ); + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-order-notes-controller.php b/includes/api/v1/class-wc-rest-order-notes-controller.php new file mode 100644 index 00000000000..4cea9c9c9a5 --- /dev/null +++ b/includes/api/v1/class-wc-rest-order-notes-controller.php @@ -0,0 +1,439 @@ +/notes endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Notes controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Controller + */ +class WC_REST_Order_Notes_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/notes'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order'; + + /** + * Register the routes for order notes. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'note' => array( + 'type' => 'string', + 'description' => __( 'Order note content.', 'woocommerce' ), + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read order notes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create order notes. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'read', $order->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a order note. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $order->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get order notes from an order. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $args = array( + 'post_id' => $order->get_id(), + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $data = array(); + foreach ( $notes as $note ) { + $order_note = $this->prepare_item_for_response( $note, $request ); + $order_note = $this->prepare_response_for_collection( $order_note ); + $data[] = $order_note; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Create the note. + $note_id = $order->add_order_note( $request['note'], $request['customer_note'] ); + + if ( ! $note_id ) { + return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $note = get_comment( $note_id ); + $this->update_additional_fields_for_object( $note, $request ); + + /** + * Fires after a order note is created or updated via the REST API. + * + * @param WP_Comment $note New order note object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( 'woocommerce_rest_insert_order_note', $note, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $note, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) ); + + return $response; + } + + /** + * Get a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $note = get_comment( $id ); + + if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $order_note = $this->prepare_item_for_response( $note, $request ); + $response = rest_ensure_response( $order_note ); + + return $response; + } + + /** + * Delete a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $note = get_comment( $id ); + + if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $note, $request ); + + $result = wp_delete_comment( $note->comment_ID, true ); + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a order note is deleted or trashed via the REST API. + * + * @param WP_Comment $note The deleted or trashed order note. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request ); + + return $response; + } + + /** + * Prepare a single order note output for response. + * + * @param WP_Comment $note Order note object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $note, $request ) { + $data = array( + 'id' => (int) $note->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $note ) ); + + /** + * Filter order note object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $note Order note object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $note Delivery order_note object. + * @return array Links for the given order note. + */ + protected function prepare_links( $note ) { + $order_id = (int) $note->comment_post_ID; + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $note->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Order Notes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'order_note', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'note' => array( + 'description' => __( 'Order note.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_note' => array( + 'description' => __( 'Shows/define if the note is only for reference or for the customer (the user will be notified).', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-order-refunds-controller.php b/includes/api/v1/class-wc-rest-order-refunds-controller.php new file mode 100644 index 00000000000..1ef2be9b5f0 --- /dev/null +++ b/includes/api/v1/class-wc-rest-order-refunds-controller.php @@ -0,0 +1,523 @@ +/refunds endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Orders_V1_Controller + */ +class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/refunds'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order_refund'; + + /** + * Order refunds actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' ); + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for order refunds. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => true, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Prepare a single order refund output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $post, $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + $refund = wc_get_order( $post ); + + if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $dp = $request['dp']; + + $data = array( + 'id' => $refund->get_id(), + 'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ), + 'amount' => wc_format_decimal( $refund->get_amount(), $dp ), + 'reason' => $refund->get_reason(), + 'line_items' => array(), + ); + + // Add line items. + foreach ( $refund->get_items() as $item_id => $item ) { + $product = $refund->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $data['line_items'][] = $line_item; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $refund, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order_Refund $refund Comment object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given order refund. + */ + protected function prepare_links( $refund, $request ) { + $order_id = $refund->get_parent_id(); + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), + ), + ); + + return $links; + } + + /** + * Query args. + * + * @param array $args Request args. + * @param WP_REST_Request $request Request object. + * @return array + */ + public function query_args( $args, $request ) { + $args['post_status'] = array_keys( wc_get_order_statuses() ); + $args['post_parent__in'] = array( absint( $request['order_id'] ) ); + + return $args; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_data = get_post( (int) $request['order_id'] ); + + if ( empty( $order_data ) ) { + return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( array( + 'order_id' => $order_data->ID, + 'amount' => $request['amount'], + 'reason' => empty( $request['reason'] ) ? null : $request['reason'], + 'line_items' => $request['line_items'], + 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, + 'restock_items' => true, + ) ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + $post = get_post( $refund->get_id() ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'Refund amount.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reason' => array( + 'description' => __( 'Reason for refund.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['dp'] = array( + 'default' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-orders-controller.php b/includes/api/v1/class-wc-rest-orders-controller.php new file mode 100644 index 00000000000..55766ab5d97 --- /dev/null +++ b/includes/api/v1/class-wc-rest-orders-controller.php @@ -0,0 +1,1621 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for orders. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Prepare a single order output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + $order = wc_get_order( $post ); + $dp = $request['dp']; + + $data = array( + 'id' => $order->get_id(), + 'parent_id' => $order->get_parent_id(), + 'status' => $order->get_status(), + 'order_key' => $order->get_order_key(), + 'number' => $order->get_order_number(), + 'currency' => $order->get_currency(), + 'version' => $order->get_version(), + 'prices_include_tax' => $order->get_prices_include_tax(), + 'date_created' => wc_rest_prepare_date_response( $order->get_date_created() ), // v1 API used UTC. + 'date_modified' => wc_rest_prepare_date_response( $order->get_date_modified() ), // v1 API used UTC. + 'customer_id' => $order->get_customer_id(), + 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'discount_tax' => wc_format_decimal( $order->get_discount_tax(), $dp ), + 'shipping_total' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'billing' => array(), + 'shipping' => array(), + 'payment_method' => $order->get_payment_method(), + 'payment_method_title' => $order->get_payment_method_title(), + 'transaction_id' => $order->get_transaction_id(), + 'customer_ip_address' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'created_via' => $order->get_created_via(), + 'customer_note' => $order->get_customer_note(), + 'date_completed' => wc_rest_prepare_date_response( $order->get_date_completed(), false ), // v1 API used local time. + 'date_paid' => wc_rest_prepare_date_response( $order->get_date_paid(), false ), // v1 API used local time. + 'cart_hash' => $order->get_cart_hash(), + 'line_items' => array(), + 'tax_lines' => array(), + 'shipping_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + 'refunds' => array(), + ); + + // Add addresses. + $data['billing'] = $order->get_address( 'billing' ); + $data['shipping'] = $order->get_address( 'shipping' ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $order->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $data['line_items'][] = $line_item; + } + + // Add taxes. + foreach ( $order->get_items( 'tax' ) as $key => $tax ) { + $tax_line = array( + 'id' => $key, + 'rate_code' => $tax['name'], + 'rate_id' => $tax['rate_id'], + 'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'], + 'compound' => (bool) $tax['compound'], + 'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ), + 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), + ); + + $data['tax_lines'][] = $tax_line; + } + + // Add shipping. + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $shipping_line = array( + 'id' => $shipping_item_id, + 'method_title' => $shipping_item['name'], + 'method_id' => $shipping_item['method_id'], + 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), + 'total_tax' => wc_format_decimal( '', $dp ), + 'taxes' => array(), + ); + + $shipping_taxes = $shipping_item->get_taxes(); + + if ( ! empty( $shipping_taxes['total'] ) ) { + $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes['total'] ), $dp ); + + foreach ( $shipping_taxes['total'] as $tax_rate_id => $tax ) { + $shipping_line['taxes'][] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + ); + } + } + + $data['shipping_lines'][] = $shipping_line; + } + + // Add fees. + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $fee_line = array( + 'id' => $fee_item_id, + 'name' => $fee_item['name'], + 'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '', + 'tax_status' => 'taxable', + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + 'taxes' => array(), + ); + + $fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] ); + if ( isset( $fee_line_taxes['total'] ) ) { + $fee_tax = array(); + + foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + if ( isset( $fee_line_taxes['subtotal'] ) ) { + foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + } + + $fee_line['taxes'] = array_values( $fee_tax ); + } + + $data['fee_lines'][] = $fee_line; + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item['name'], + 'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), + 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), + ); + + $data['coupon_lines'][] = $coupon_line; + } + + // Add refunds. + foreach ( $order->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'refund' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $dp ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $order, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order $order Order object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given order. + */ + protected function prepare_links( $order, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + if ( 0 !== (int) $order->get_user_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ), + ); + } + if ( 0 !== (int) $order->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ), + ); + } + return $links; + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + global $wpdb; + + // Set post_status. + if ( 'any' !== $request['status'] ) { + $args['post_status'] = 'wc-' . $request['status']; + } else { + $args['post_status'] = 'any'; + } + + if ( isset( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $request['product'] ) ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + return $args; + } + + /** + * Prepare a single order for create. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WC_Order $data Object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'billing' : + case 'shipping' : + $this->update_address( $order, $value, $key ); + break; + case 'line_items' : + case 'shipping_lines' : + case 'fee_lines' : + case 'coupon_lines' : + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + default : + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the data for the insert. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WC_Order $order The prder object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); + } + + /** + * Create base WC Order object. + * @deprecated 3.0.0 + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * Only reutrn writeable props from schema. + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Create order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + try { + // Make sure customer exists. + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $order = $this->prepare_item_for_database( $request ); + $order->set_created_via( 'rest-api' ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->calculate_totals(); + $order->save(); + + // Handle set paid. + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function update_order( $request ) { + try { + $order = $this->prepare_item_for_database( $request ); + $order->save(); + + // Handle set paid. + if ( $order->needs_payment() && true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $order->calculate_totals(); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @param array $posted Request data + * + * @return int + * @throws WC_REST_Exception + */ + protected function get_product_id( $posted ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * @param WC_Order_Item $item + * @param string $prop + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * @param WC_Order_Item $item + * @param string[] $props + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } + } + + /** + * Create or update a line item. + * + * @param array $posted Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * + * @return WC_Order_Item_Product + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_line_items( $posted, $action = 'create' ) { + $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + $product = wc_get_product( $this->get_product_id( $posted ) ); + + if ( $product !== $item->get_product() ) { + $item->set_product( $product ); + + if ( 'create' === $action ) { + $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $item->set_total( $total ); + $item->set_subtotal( $total ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); + + return $item; + } + + /** + * Create or update an order shipping method. + * + * @param $posted $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * + * @return WC_Order_Item_Shipping + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_shipping_lines( $posted, $action ) { + $item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted ); + + return $item; + } + + /** + * Create or update an order fee. + * + * @param array $posted Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * + * @return WC_Order_Item_Fee + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_fee_lines( $posted, $action ) { + $item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); + + return $item; + } + + /** + * Create or update an order coupon. + * + * @param array $posted Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * + * @return WC_Order_Item_Coupon + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_coupon_lines( $posted, $action ) { + $item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); + + return $item; + } + + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order + * @param string $item_type + * @param array $posted item provided in the request body + * @throws WC_REST_Exception If item ID is not associated with order + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $posted['id'] ), + absint( $order->get_id() ) + ) ); + if ( is_null( $result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data + $item = $this->$method( $posted, $action ); + + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // Save or add to order + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_id = $this->create_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Update a single order. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $post_id = (int) $request['id']; + + if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $order_id = $this->update_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get order statuses without prefixes. + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $this->get_order_statuses(), + 'context' => array( 'view', 'edit' ), + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'currency' => array( + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'default' => get_woocommerce_currency(), + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'version' => array( + 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'User agent of the customer.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid' => array( + 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Shipping lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'fee_lines' => array( + 'description' => __( 'Fee lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'taxable', 'none' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Coupons line data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'refunds' => array( + 'description' => __( 'List of refunds.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Refund ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reason' => array( + 'description' => __( 'Refund reason.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Refund total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-product-attribute-terms-controller.php b/includes/api/v1/class-wc-rest-product-attribute-terms-controller.php new file mode 100644 index 00000000000..e11ba3dace6 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-attribute-terms-controller.php @@ -0,0 +1,240 @@ +/terms endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Attribute Terms controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Terms_Controller + */ +class WC_REST_Product_Attribute_Terms_V1_Controller extends WC_REST_Terms_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/attributes/(?P[\d]+)/terms'; + + /** + * Register the routes for terms. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'attribute_id' => array( + 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'Name for the resource.', 'woocommerce' ), + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + )); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + 'attribute_id' => array( + 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'attribute_id' => array( + 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Prepare a single product attribute term output for response. + * + * @param WP_Term $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + // Get term order. + $menu_order = get_woocommerce_term_meta( $item->term_id, 'order_' . $this->taxonomy ); + + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Update term meta fields. + * + * @param WP_Term $term + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + update_woocommerce_term_meta( $id, 'order_' . $this->taxonomy, $request['menu_order'] ); + + return true; + } + + /** + * Get the Attribute Term's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_attribute_term', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-attributes-controller.php b/includes/api/v1/class-wc-rest-product-attributes-controller.php new file mode 100644 index 00000000000..32fe92f5189 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-attributes-controller.php @@ -0,0 +1,663 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'name' => array( + 'description' => __( 'Name for the resource.', 'woocommerce' ), + 'type' => 'string', + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + )); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => true, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check if a given request has access to read the attributes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! $this->get_taxonomy( $request ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! $this->get_taxonomy( $request ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! $this->get_taxonomy( $request ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all attributes. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $attributes = wc_get_attribute_taxonomies(); + $data = array(); + foreach ( $attributes as $attribute_obj ) { + $attribute = $this->prepare_item_for_response( $attribute_obj, $request ); + $attribute = $this->prepare_response_for_collection( $attribute ); + $data[] = $attribute; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + global $wpdb; + + $args = array( + 'attribute_label' => $request['name'], + 'attribute_name' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ), + 'attribute_type' => ! empty( $request['type'] ) ? $request['type'] : 'select', + 'attribute_orderby' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order', + 'attribute_public' => true === $request['has_archives'], + ); + + // Set the attribute slug. + if ( empty( $args['attribute_name'] ) ) { + $args['attribute_name'] = wc_sanitize_taxonomy_name( stripslashes( $args['attribute_label'] ) ); + } else { + $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); + } + + $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], true ); + if ( is_wp_error( $valid_slug ) ) { + return $valid_slug; + } + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $args, + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for errors. + if ( is_wp_error( $insert ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', $insert->get_error_message(), array( 'status' => 400 ) ); + } + + $attribute = $this->get_attribute( $wpdb->insert_id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $this->update_additional_fields_for_object( $attribute, $request ); + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) ); + + // Clear transients. + $this->flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return $response; + } + + /** + * Get a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function get_item( $request ) { + $attribute = $this->get_attribute( (int) $request['id'] ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $response = $this->prepare_item_for_response( $attribute, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Update a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $format = array( '%s', '%s', '%s', '%s', '%d' ); + $args = array( + 'attribute_label' => $request['name'], + 'attribute_name' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ), + 'attribute_type' => $request['type'], + 'attribute_orderby' => $request['order_by'], + 'attribute_public' => $request['has_archives'], + ); + + $i = 0; + foreach ( $args as $key => $value ) { + if ( empty( $value ) && ! is_bool( $value ) ) { + unset( $args[ $key ] ); + unset( $format[ $i ] ); + } + + $i++; + } + + // Set the attribute slug. + if ( ! empty( $args['attribute_name'] ) ) { + $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); + + $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], false ); + if ( is_wp_error( $valid_slug ) ) { + return $valid_slug; + } + } + + if ( $args ) { + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $args, + array( 'attribute_id' => $id ), + $format, + array( '%d' ) + ); + + // Checks for errors. + if ( false === $update ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Could not edit the attribute.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + $attribute = $this->get_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $this->update_additional_fields_for_object( $attribute, $request ); + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + + // Clear transients. + $this->flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + global $wpdb; + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $attribute = $this->get_attribute( (int) $request['id'] ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $attribute->attribute_id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute->attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + /** + * Fires after a single attribute is deleted via the REST API. + * + * @param stdObject $attribute The deleted attribute. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request ); + + // Fires woocommerce_attribute_deleted hook. + do_action( 'woocommerce_attribute_deleted', $attribute->attribute_id, $attribute->attribute_name, $taxonomy ); + + // Clear transients. + $this->flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return $response; + } + + /** + * Prepare a single product attribute output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item->attribute_id, + 'name' => $item->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $item->attribute_name ), + 'type' => $item->attribute_type, + 'order_by' => $item->attribute_orderby, + 'has_archives' => (bool) $item->attribute_public, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter a attribute item returned from the API. + * + * Allows modification of the product attribute data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original attribute object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $attribute Attribute object. + * @return array Links for the given attribute. + */ + protected function prepare_links( $attribute ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + + return $links; + } + + /** + * Get the Attribute's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_attribute', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'type' => array( + 'description' => __( 'Type of attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'select', + 'enum' => array_keys( wc_get_attribute_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'order_by' => array( + 'description' => __( 'Default sort order.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'menu_order', + 'enum' => array( 'menu_order', 'name', 'name_num', 'id' ), + 'context' => array( 'view', 'edit' ), + ), + 'has_archives' => array( + 'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + + return $params; + } + + /** + * Get attribute name. + * + * @param WP_REST_Request $request Full details about the request. + * @return string + */ + protected function get_taxonomy( $request ) { + if ( '' !== $this->attribute ) { + return $this->attribute; + } + + if ( $request['id'] ) { + $name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] ); + + $this->attribute = $name; + } + + return $this->attribute; + } + + /** + * Get attribute data. + * + * @param int $id Attribute ID. + * @return stdClass|WP_Error + */ + protected function get_attribute( $id ) { + global $wpdb; + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + return new WP_Error( 'woocommerce_rest_attribute_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $attribute; + } + + /** + * Validate attribute slug. + * + * @param string $slug + * @param bool $new_data + * @return bool|WP_Error + */ + protected function validate_attribute_slug( $slug, $new_data = true ) { + if ( strlen( $slug ) >= 28 ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } + + return true; + } + + /** + * Schedule to flush rewrite rules. + * + * @since 3.0.0 + */ + protected function flush_rewrite_rules() { + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-categories-controller.php b/includes/api/v1/class-wc-rest-product-categories-controller.php new file mode 100644 index 00000000000..182dc482bb2 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-categories-controller.php @@ -0,0 +1,267 @@ +term_id, 'display_type' ); + + // Get category order. + $menu_order = get_woocommerce_term_meta( $item->term_id, 'order' ); + + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'parent' => (int) $item->parent, + 'description' => $item->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => array(), + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + if ( $image_id = get_woocommerce_term_meta( $item->term_id, 'thumbnail_id' ) ) { + $attachment = get_post( $image_id ); + + $data['image'] = array( + 'id' => (int) $image_id, + 'date_created' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), + 'src' => wp_get_attachment_url( $image_id ), + 'title' => get_the_title( $attachment ), + 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Update term meta fields. + * + * @param WP_Term $term + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + if ( isset( $request['display'] ) ) { + update_woocommerce_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); + } + + if ( isset( $request['menu_order'] ) ) { + update_woocommerce_term_meta( $id, 'order', $request['menu_order'] ); + } + + if ( isset( $request['image'] ) ) { + if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) ); + + if ( is_wp_error( $upload ) ) { + return $upload; + } + + $image_id = wc_rest_set_uploaded_image_as_attachment( $upload ); + } else { + $image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0; + } + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_woocommerce_term_meta( $id, 'thumbnail_id', $image_id ); + + // Set the image alt. + if ( ! empty( $request['image']['alt'] ) ) { + update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) ); + } + + // Set the image title. + if ( ! empty( $request['image']['title'] ) ) { + wp_update_post( array( 'ID' => $image_id, 'post_title' => wc_clean( $request['image']['title'] ) ) ); + } + } else { + delete_woocommerce_term_meta( $id, 'thumbnail_id' ); + } + } + + return true; + } + + /** + * Get the Category schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'parent' => array( + 'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'display' => array( + 'description' => __( 'Category archive display type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'default', + 'enum' => array( 'default', 'products', 'subcategories', 'both' ), + 'context' => array( 'view', 'edit' ), + ), + 'image' => array( + 'description' => __( 'Image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-reviews-controller.php b/includes/api/v1/class-wc-rest-product-reviews-controller.php new file mode 100644 index 00000000000..12630df66f7 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-reviews-controller.php @@ -0,0 +1,570 @@ +/reviews. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Reviews Controller Class. + * + * @package WooCommerce/API + * @extends WC_REST_Controller + */ +class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'review' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Review content.', 'woocommerce' ), + ), + 'name' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Name of the reviewer.', 'woocommerce' ), + ), + 'email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Email of the reviewer.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'product', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'product', 'read', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a new product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'create', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to update a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'edit', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to delete a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'delete', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get all reviews from a product. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $reviews = get_approved_comments( $product_id ); + $data = array(); + foreach ( $reviews as $review_data ) { + $review = $this->prepare_item_for_response( $review_data, $request ); + $review = $this->prepare_response_for_collection( $review ); + $data[] = $review; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $id ); + + if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + + /** + * Create a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + + /** + * Filter a product review (comment) before it is inserted via the REST API. + * + * Allows modification of the comment right before it is inserted via `wp_insert_comment`. + * + * @param array $prepared_review The prepared comment data for `wp_insert_comment`. + * @param WP_REST_Request $request Request used to insert the comment. + */ + $prepared_review = apply_filters( 'rest_pre_insert_product_review', $prepared_review, $request ); + + $product_review_id = wp_insert_comment( $prepared_review ); + if ( ! $product_review_id ) { + return new WP_Error( 'rest_product_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + update_comment_meta( $product_review_id, 'rating', ( ! empty( $request['rating'] ) ? $request['rating'] : '0' ) ); + + $product_review = get_comment( $product_review_id ); + $this->update_additional_fields_for_object( $product_review, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $product_review Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $product_review_id ) ) ); + + return $response; + } + + /** + * Update a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $product_review_id = (int) $request['id']; + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $product_review_id ); + + if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + + $updated = wp_update_comment( $prepared_review ); + if ( 0 === $updated ) { + return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( ! empty( $request['rating'] ) ) { + update_comment_meta( $product_review_id, 'rating', $request['rating'] ); + } + + $product_review = get_comment( $product_review_id ); + $this->update_additional_fields_for_object( $product_review, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $comment Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a product review. + * + * @param WP_REST_Request $request Full details about the request + * + * @return bool|WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $product_review_id = absint( is_array( $request['id'] ) ? $request['id']['id'] : $request['id'] ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + $product_review = get_comment( $product_review_id ); + if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + /** + * Filter whether a product review is trashable. + * + * Return false to disable trash support for the product review. + * + * @param boolean $supports_trash Whether the object supports trashing. + * @param WP_Post $product_review The object being considered for trashing support. + */ + $supports_trash = apply_filters( 'rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $product_review ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + + if ( $force ) { + $result = wp_delete_comment( $product_review_id, true ); + } else { + if ( ! $supports_trash ) { + return new WP_Error( 'rest_trash_not_supported', __( 'The product review does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + if ( 'trash' === $product_review->comment_approved ) { + return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); + } + + $result = wp_trash_comment( $product_review->comment_ID ); + } + + if ( ! $result ) { + return new WP_Error( 'rest_cannot_delete', __( 'The product review cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a product review is deleted via the REST API. + * + * @param object $product_review The deleted item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'rest_delete_product_review', $product_review, $response, $request ); + + return $response; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), + 'name' => $review->comment_author, + 'email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + /** + * Prepare a single product review to be inserted into the database. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error $prepared_review + */ + protected function prepare_item_for_database( $request ) { + $prepared_review = array( 'comment_approved' => 1, 'comment_type' => 'review' ); + + if ( isset( $request['id'] ) ) { + $prepared_review['comment_ID'] = (int) $request['id']; + } + + if ( isset( $request['review'] ) ) { + $prepared_review['comment_content'] = $request['review']; + } + + if ( isset( $request['product_id'] ) ) { + $prepared_review['comment_post_ID'] = (int) $request['product_id']; + } + + if ( isset( $request['name'] ) ) { + $prepared_review['comment_author'] = $request['name']; + } + + if ( isset( $request['email'] ) ) { + $prepared_review['comment_author_email'] = $request['email']; + } + + return apply_filters( 'rest_preprocess_product_review', $prepared_review, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given product review. + */ + protected function prepare_links( $review, $request ) { + $product_id = (int) $request['product_id']; + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-shipping-classes-controller.php b/includes/api/v1/class-wc-rest-product-shipping-classes-controller.php new file mode 100644 index 00000000000..445c109df27 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-shipping-classes-controller.php @@ -0,0 +1,134 @@ + (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Shipping Class schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Shipping class name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-tags-controller.php b/includes/api/v1/class-wc-rest-product-tags-controller.php new file mode 100644 index 00000000000..accdfae4c63 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-tags-controller.php @@ -0,0 +1,134 @@ + (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Tag's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/v1/class-wc-rest-products-controller.php b/includes/api/v1/class-wc-rest-products-controller.php new file mode 100644 index 00000000000..f6a706c7bfa --- /dev/null +++ b/includes/api/v1/class-wc-rest-products-controller.php @@ -0,0 +1,2640 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + add_action( "woocommerce_rest_insert_{$this->post_type}", array( $this, 'clear_transients' ) ); + } + + /** + * Register the routes for products. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'type' => 'boolean', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get post types. + * + * @return array + */ + protected function get_post_types() { + return array( 'product', 'product_variation' ); + } + + /** + * Query args. + * + * @param array $args Request args. + * @param WP_REST_Request $request Request data. + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) && is_array( $request[ $key ] ) ) { + $request[ $key ] = array_filter( $request[ $key ] ); + } + + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) ); + } + + // Apply all WP_Query filters again. + if ( is_array( $request['filter'] ) ) { + $args = array_merge( $args, $request['filter'] ); + unset( $args['filter'] ); + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + return $args; + } + + /** + * Get the downloads for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_downloads() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param string $taxonomy Taxonomy slug. + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + return $terms; + } + + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + // Add featured image. + if ( has_post_thumbnail( $product->get_id() ) ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + $images[] = array( + 'id' => 0, + 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), + 'src' => wc_placeholder_img_src(), + 'name' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get attribute taxonomy label. + * + * @param string $name Taxonomy name. + * @return string + */ + protected function get_attribute_taxonomy_label( $name ) { + $tax = get_taxonomy( $name ); + $labels = get_taxonomy_labels( $tax ); + + return $labels->singular_name; + } + + /** + * Get default attributes. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { + if ( 0 === strpos( $key, 'pa_' ) ) { + $default[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $key ), + 'name' => $this->get_attribute_taxonomy_label( $key ), + 'option' => $value, + ); + } else { + $default[] = array( + 'id' => 0, + 'name' => str_replace( 'pa_', '', $key ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id Product ID. + * @param array $attribute Attribute data. + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + // Variation attributes. + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + $name = str_replace( 'attribute_', '', $attribute_name ); + + if ( ! $attribute ) { + continue; + } + + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $name ); + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $name ), + 'name' => $this->get_attribute_taxonomy_label( $name ), + 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $name, + 'option' => $attribute, + ); + } + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + if ( $attribute['is_taxonomy'] ) { + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $attribute['name'] ), + 'name' => $this->get_attribute_taxonomy_label( $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $attribute['name'], + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + } + + return $attributes; + } + + /** + * Get product menu order. + * + * @deprecated 3.0.0 + * @param WC_Product $product Product instance. + * @return int + */ + protected function get_product_menu_order( $product ) { + return $product->get_menu_order(); + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_product_data( $product ) { + $data = array( + 'id' => $product->get_id(), + 'name' => $product->get_name(), + 'slug' => $product->get_slug(), + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified() ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'description' => wpautop( do_shortcode( $product->get_description() ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', + 'date_on_sale_from' => $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '', + 'date_on_sale_to' => $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '', + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale(), + 'purchasable' => $product->is_purchasable(), + 'total_sales' => $product->get_total_sales(), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'download_type' => 'standard', + 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'manage_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders' => $product->get_backorders(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'weight' => $product->get_weight(), + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => $product->get_shipping_class_id(), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'parent_id' => $product->get_parent_id(), + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'categories' => $this->get_taxonomy_terms( $product ), + 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), + 'images' => $this->get_images( $product ), + 'attributes' => $this->get_attributes( $product ), + 'default_attributes' => $this->get_default_attributes( $product ), + 'variations' => array(), + 'grouped_products' => array(), + 'menu_order' => $product->get_menu_order(), + ); + + return $data; + } + + /** + * Get an individual variation's data. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_variation_data( $product ) { + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + $variation = wc_get_product( $child_id ); + if ( ! $variation || ! $variation->exists() ) { + continue; + } + + $variations[] = array( + 'id' => $variation->get_id(), + 'date_created' => wc_rest_prepare_date_response( $variation->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $variation->get_date_modified() ), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price(), + 'date_on_sale_from' => $variation->get_date_on_sale_from() ? date( 'Y-m-d', $variation->get_date_on_sale_from()->getTimestamp() ) : '', + 'date_on_sale_to' => $variation->get_date_on_sale_to() ? date( 'Y-m-d', $variation->get_date_on_sale_to()->getTimestamp() ) : '', + 'on_sale' => $variation->is_on_sale(), + 'purchasable' => $variation->is_purchasable(), + 'visible' => $variation->is_visible(), + 'virtual' => $variation->is_virtual(), + 'downloadable' => $variation->is_downloadable(), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => '' !== $variation->get_download_limit() ? (int) $variation->get_download_limit() : -1, + 'download_expiry' => '' !== $variation->get_download_expiry() ? (int) $variation->get_download_expiry() : -1, + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'manage_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders' => $variation->get_backorders(), + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'weight' => $variation->get_weight(), + 'dimensions' => array( + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + ), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => $variation->get_shipping_class_id(), + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + ); + } + + return $variations; + } + + /** + * Prepare a single product output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $post, $request ) { + $product = wc_get_product( $post ); + $data = $this->get_product_data( $product ); + + // Add variations to variable products. + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $data['variations'] = $this->get_variation_data( $product ); + } + + // Add grouped products data. + if ( $product->is_type( 'grouped' ) && $product->has_child() ) { + $data['grouped_products'] = $product->get_children(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $product, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Product $product Product object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given product. + */ + protected function prepare_links( $product, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( $product->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param WC_Product $product An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $product, $request ); + } + + /** + * Create a single product. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $product_id = 0; + + try { + $product_id = $this->save_product( $request ); + $post = get_post( $product_id ); + $this->update_additional_fields_for_object( $post, $request ); + $this->update_post_meta_fields( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( 'woocommerce_rest_insert_product', $post, $request, true ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } catch ( WC_Data_Exception $e ) { + $this->delete_post( $product_id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + $this->delete_post( $product_id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update a single product. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $post_id = (int) $request['id']; + + if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + try { + $product_id = $this->save_product( $request ); + $post = get_post( $product_id ); + $this->update_additional_fields_for_object( $post, $request ); + $this->update_post_meta_fields( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( 'woocommerce_rest_insert_product', $post, $request, false ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + return rest_ensure_response( $response ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Saves a product to the database. + * + * @param WP_REST_Request $request Full details about the request. + * @return int + */ + public function save_product( $request ) { + $product = $this->prepare_item_for_database( $request ); + return $product->save(); + } + + /** + * Save product images. + * + * @deprecated 3.0.0 + * @param int $product_id + * @param array $images + * @throws WC_REST_Exception + */ + protected function save_product_images( $product_id, $images ) { + $product = wc_get_product( $product_id ); + + return set_product_images( $product, $images ); + } + + /** + * Set product images. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Save product shipping data. + * + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * @return WC_Product + */ + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( $data['dimensions']['height'] ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( $data['dimensions']['width'] ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( $data['dimensions']['length'] ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + $shipping_class_term = get_term_by( 'slug', wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); + + if ( $shipping_class_term ) { + $product->set_shipping_class_id( $shipping_class_term->term_id ); + } + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Save product meta. + * + * @deprecated 3.0.0 + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_product_meta( $product, $request ) { + $product = $this->set_product_meta( $product, $request ); + $product->save(); + + return true; + } + + /** + * Set product meta. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return WC_Product + */ + protected function set_product_meta( $product, $request ) { + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + } + + // Product parent ID for groups. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + return $product; + } + + /** + * Save variations. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return bool + */ + protected function save_variations_data( $product, $request ) { + foreach ( $request['variations'] as $menu_order => $data ) { + $variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 ); + + // Create initial name and status. + if ( ! $variation->get_slug() ) { + /* translators: 1: variation id 2: product name */ + $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); + $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); + } + + // Parent ID. + $variation->set_parent_id( $product->get_id() ); + + // Menu order. + $variation->set_menu_order( $menu_order ); + + // Status. + if ( isset( $data['visible'] ) ) { + $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { + $image = $data['image']; + $image = current( $image ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->set_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $variation->set_downloadable( $data['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + if ( isset( $data['manage_stock'] ) ) { + $variation->set_manage_stock( $data['manage_stock'] ); + } + + if ( isset( $data['in_stock'] ) ) { + $variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' ); + } + + if ( isset( $data['backorders'] ) ) { + $variation->set_backorders( $data['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $data['date_on_sale_from'] ); + } + + if ( isset( $data['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $data['date_on_sale_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Description. + if ( isset( $data['description'] ) ) { + $variation->set_description( wp_kses_post( $data['description'] ) ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + $parent_attributes = $product->get_attributes(); + + foreach ( $data['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data ); + } + + return true; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request data. + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return $this->update_post_meta_fields( $post, $request ); + } + + /** + * Update post meta fields. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request data. + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $product = $this->set_product_meta( $product, $request ); + + // Save the product data. + $product->save(); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations_data( $product, $request ); + } + } + + // Clear caches here so in sync with any new variations/children. + wc_delete_product_transients( $product->get_id() ); + wp_cache_delete( 'product-' . $product->get_id(), 'products' ); + + return true; + } + + /** + * Clear cache/transients. + * + * @param WP_Post $post Post data. + */ + public function clear_transients( $post ) { + wc_delete_product_transients( $post->ID ); + } + + /** + * Delete post. + * + * @param int|WP_Post $id Post ID or WP_Post instance. + */ + protected function delete_post( $id ) { + if ( ! empty( $id->ID ) ) { + $id = $id->ID; + } elseif ( ! is_numeric( $id ) || 0 >= $id ) { + return; + } + + // Delete product attachments. + $attachments = get_posts( array( + 'post_parent' => $id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product. + $product = wc_get_product( $id ); + $product->delete( true ); + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $post = get_post( $id ); + $product = wc_get_product( $id ); + + if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); + } elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0; + + /** + * Filter whether an item is trashable. + * + * Return false to disable trash support for the item. + * + * @param boolean $supports_trash Whether the item type support trashing. + * @param WP_Post $post The Post object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->delete( true ); + } + } elseif ( $product->is_type( 'grouped' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->set_parent_id( 0 ); + $child->save(); + } + } + + $product->delete( true ); + $result = $product->get_id() > 0 ? false : true; + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( 'trash' === $post->post_status ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + // (Note that internally this falls through to `wp_delete_post` if + // the trash is disabled.) + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param object $post The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); + + return $response; + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_keys( get_post_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( 'End data of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_type' => array( + 'description' => __( 'Download type, this controls the schema on the front-end.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'standard', + 'enum' => array( 'standard' ), + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of upsell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Variation ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'permalink' => array( + 'description' => __( 'Variation URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current variation price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Variation regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Variation sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( 'End data of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'on_sale' => array( + 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'visible' => array( + 'description' => __( 'If the variation is visible.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'virtual' => array( + 'description' => __( 'If the variation is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at variation level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Variation dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'image' => array( + 'description' => __( 'Variation image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['slug'] = array( + 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), array_keys( get_post_statuses() ) ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['type'] = array( + 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_types() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( + 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute'] = array( + 'description' => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute_term'] = array( + 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-report-sales-controller.php b/includes/api/v1/class-wc-rest-report-sales-controller.php new file mode 100644 index 00000000000..84902d85b41 --- /dev/null +++ b/includes/api/v1/class-wc-rest-report-sales-controller.php @@ -0,0 +1,397 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read report. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get sales reports. + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $item = $this->prepare_item_for_response( null, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report sales object for serialization. + * + * @param null $_ + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $_, $request ) { + // Set date filtering. + $filter = array( + 'period' => $request['period'], + 'date_min' => $request['date_min'], + 'date_max' => $request['date_max'], + ); + $this->setup_report( $filter ); + + // New customers. + $users_query = new WP_User_Query( + array( + 'fields' => array( 'user_registered' ), + 'role' => 'customer', + ) + ); + + $customers = $users_query->get_results(); + + foreach ( $customers as $key => $customer ) { + if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { + unset( $customers[ $key ] ); + } + } + + $total_customers = count( $customers ); + $report_data = $this->report->get_report_data(); + $period_totals = array(); + + // Setup period totals by ensuring each period in the interval has data. + for ( $i = 0; $i <= $this->report->chart_interval; $i++ ) { + + switch ( $this->report->chart_groupby ) { + case 'day' : + $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); + break; + default : + $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); + break; + } + + // Set the customer signups for each period. + $customer_count = 0; + foreach ( $customers as $customer ) { + if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { + $customer_count++; + } + } + + $period_totals[ $time ] = array( + 'sales' => wc_format_decimal( 0.00, 2 ), + 'orders' => 0, + 'items' => 0, + 'tax' => wc_format_decimal( 0.00, 2 ), + 'shipping' => wc_format_decimal( 0.00, 2 ), + 'discount' => wc_format_decimal( 0.00, 2 ), + 'customers' => $customer_count, + ); + } + + // add total sales, total order count, total tax and total shipping for each period + foreach ( $report_data->orders as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); + $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); + $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); + } + + foreach ( $report_data->order_counts as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['orders'] = (int) $order->count; + } + + // Add total order items for each period. + foreach ( $report_data->order_items as $order_item ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; + } + + // Add total discount for each period. + foreach ( $report_data->coupons as $discount ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); + } + + $sales_data = array( + 'total_sales' => $report_data->total_sales, + 'net_sales' => $report_data->net_sales, + 'average_sales' => $report_data->average_sales, + 'total_orders' => $report_data->total_orders, + 'total_items' => $report_data->total_items, + 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), + 'total_shipping' => $report_data->total_shipping, + 'total_refunds' => $report_data->total_refunds, + 'total_discount' => $report_data->total_coupons, + 'totals_grouped_by' => $this->report->chart_groupby, + 'totals' => $period_totals, + 'total_customers' => $total_customers, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $sales_data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'about' => array( + 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), + ), + ) ); + + /** + * Filter a report sales returned from the API. + * + * Allows modification of the report sales data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $data The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $sales_data, $request ); + } + + /** + * Setup the report object and parse any date filtering. + * + * @param array $filter date filtering + */ + protected function setup_report( $filter ) { + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); + + $this->report = new WC_Report_Sales_By_Date(); + + if ( empty( $filter['period'] ) ) { + // Custom date range. + $filter['period'] = 'custom'; + + if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { + + // Overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges. + $_GET['start_date'] = $filter['date_min']; + $_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null; + + } else { + + // Default custom range to today. + $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); + } + } else { + $filter['period'] = empty( $filter['period'] ) ? 'week' : $filter['period']; + + // Change "week" period to "7day". + if ( 'week' === $filter['period'] ) { + $filter['period'] = '7day'; + } + } + + $this->report->calculate_current_range( $filter['period'] ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'sales_report', + 'type' => 'object', + 'properties' => array( + 'total_sales' => array( + 'description' => __( 'Gross sales in the period.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'net_sales' => array( + 'description' => __( 'Net sales in the period.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'average_sales' => array( + 'description' => __( 'Average net daily sales.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_orders' => array( + 'description' => __( 'Total of orders placed.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_items' => array( + 'description' => __( 'Total of items purchased.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Total charged for taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_shipping' => array( + 'description' => __( 'Total charged for shipping.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_refunds' => array( + 'description' => __( 'Total of refunded orders.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_discount' => array( + 'description' => __( 'Total of coupons used.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'totals_grouped_by' => array( + 'description' => __( 'Group type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'totals' => array( + 'description' => __( 'Totals.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'array', + ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'period' => array( + 'description' => __( 'Report period.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'week', 'month', 'last_month', 'year' ), + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'date_min' => array( + /* translators: %s: date format */ + 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'date_max' => array( + /* translators: %s: date format */ + 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-report-top-sellers-controller.php b/includes/api/v1/class-wc-rest-report-top-sellers-controller.php new file mode 100644 index 00000000000..de8ec7e80c9 --- /dev/null +++ b/includes/api/v1/class-wc-rest-report-top-sellers-controller.php @@ -0,0 +1,174 @@ + $request['period'], + 'date_min' => $request['date_min'], + 'date_max' => $request['date_max'], + ); + $this->setup_report( $filter ); + + $report_data = $this->report->get_order_report_data( array( + 'data' => array( + '_product_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => '', + 'name' => 'product_id', + ), + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ), + ), + 'order_by' => 'order_item_qty DESC', + 'group_by' => 'product_id', + 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + $top_sellers = array(); + + foreach ( $report_data as $item ) { + $product = wc_get_product( $item->product_id ); + + if ( $product ) { + $top_sellers[] = array( + 'name' => $product->get_name(), + 'product_id' => (int) $item->product_id, + 'quantity' => wc_stock_amount( $item->order_item_qty ), + ); + } + } + + $data = array(); + foreach ( $top_sellers as $top_seller ) { + $item = $this->prepare_item_for_response( (object) $top_seller, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report sales object for serialization. + * + * @param stdClass $top_seller + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $top_seller, $request ) { + $data = array( + 'name' => $top_seller->name, + 'product_id' => $top_seller->product_id, + 'quantity' => $top_seller->quantity, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'about' => array( + 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), + ), + 'product' => array( + 'href' => rest_url( sprintf( '/%s/products/%s', $this->namespace, $top_seller->product_id ) ), + ), + ) ); + + /** + * Filter a report top sellers returned from the API. + * + * Allows modification of the report top sellers data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $top_seller The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_top_sellers', $response, $top_seller, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'top_sellers_report', + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Total number of purchases.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/api/v1/class-wc-rest-reports-controller.php b/includes/api/v1/class-wc-rest-reports-controller.php new file mode 100644 index 00000000000..ed5dc97fef0 --- /dev/null +++ b/includes/api/v1/class-wc-rest-reports-controller.php @@ -0,0 +1,174 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read reports. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all reports. + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $reports = array( + array( + 'slug' => 'sales', + 'description' => __( 'List of sales reports.', 'woocommerce' ), + ), + array( + 'slug' => 'top_sellers', + 'description' => __( 'List of top sellers products.', 'woocommerce' ), + ), + ); + + foreach ( $reports as $report ) { + $item = $this->prepare_item_for_response( (object) $report, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'description' => $report->description, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ) ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human-readable description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-tax-classes-controller.php b/includes/api/v1/class-wc-rest-tax-classes-controller.php new file mode 100644 index 00000000000..f857256ac95 --- /dev/null +++ b/includes/api/v1/class-wc-rest-tax-classes-controller.php @@ -0,0 +1,364 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P\w[\w\s\-]*)', array( + 'args' => array( + 'slug' => array( + 'description' => __( 'Unique slug for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read tax classes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create tax classes. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all tax classes. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $tax_classes = array(); + + // Add standard class. + $tax_classes[] = array( + 'slug' => 'standard', + 'name' => __( 'Standard rate', 'woocommerce' ), + ); + + $classes = WC_Tax::get_tax_classes(); + + foreach ( $classes as $class ) { + $tax_classes[] = array( + 'slug' => sanitize_title( $class ), + 'name' => $class, + ); + } + + $data = array(); + foreach ( $tax_classes as $tax_class ) { + $class = $this->prepare_item_for_response( $tax_class, $request ); + $class = $this->prepare_response_for_collection( $class ); + $data[] = $class; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $exists = false; + $classes = WC_Tax::get_tax_classes(); + $tax_class = array( + 'slug' => sanitize_title( $request['name'] ), + 'name' => $request['name'], + ); + + // Check if class exists. + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $tax_class['slug'] ) { + $exists = true; + break; + } + } + + // Return error if tax class already exists. + if ( $exists ) { + return new WP_Error( 'woocommerce_rest_tax_class_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Add the new class. + $classes[] = $tax_class['name']; + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + $this->update_additional_fields_for_object( $tax_class, $request ); + + /** + * Fires after a tax class is created or updated via the REST API. + * + * @param stdClass $tax_class Data used to create the tax class. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax class, false when updating tax class. + */ + do_action( 'woocommerce_rest_insert_tax_class', (object) $tax_class, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax_class, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $tax_class['slug'] ) ) ); + + return $response; + } + + /** + * Delete a single tax class. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax_class = array( + 'slug' => sanitize_title( $request['slug'] ), + 'name' => '', + ); + $classes = WC_Tax::get_tax_classes(); + $deleted = false; + + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $tax_class['slug'] ) { + $tax_class['name'] = $class; + unset( $classes[ $key ] ); + $deleted = true; + break; + } + } + + if ( ! $deleted ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + // Delete tax rate locations locations from the selected class. + $wpdb->query( $wpdb->prepare( " + DELETE locations.* + FROM {$wpdb->prefix}woocommerce_tax_rate_locations AS locations + INNER JOIN + {$wpdb->prefix}woocommerce_tax_rates AS rates + ON rates.tax_rate_id = locations.tax_rate_id + WHERE rates.tax_rate_class = '%s' + ", $tax_class['slug'] ) ); + + // Delete tax rates in the selected class. + $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_class' => $tax_class['slug'] ), array( '%s' ) ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax_class, $request ); + + /** + * Fires after a tax class is deleted via the REST API. + * + * @param stdClass $tax_class The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', (object) $tax_class, $response, $request ); + + return $response; + } + + /** + * Prepare a single tax class output for response. + * + * @param array $tax_class Tax class data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax_class, $request ) { + $data = $tax_class; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links() ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax_class Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, (object) $tax_class, $request ); + } + + /** + * Prepare links for the request. + * + * @return array Links for the given tax class. + */ + protected function prepare_links() { + $links = array( + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Tax Classes schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax_class', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Tax class name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-taxes-controller.php b/includes/api/v1/class-wc-rest-taxes-controller.php new file mode 100644 index 00000000000..0f924db5674 --- /dev/null +++ b/includes/api/v1/class-wc-rest-taxes-controller.php @@ -0,0 +1,709 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create taxes. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + global $wpdb; + + $prepared_args = array(); + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'tax_rate_id', + 'order' => 'tax_rate_order', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['class'] = $request['class']; + + /** + * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. + * + * @param array $prepared_args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); + + $query = " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE 1 = 1 + "; + + // Filter by tax class. + if ( ! empty( $prepared_args['class'] ) ) { + $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; + $query .= " AND tax_rate_class = '$class'"; + } + + // Order tax rates. + $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) ); + + // Pagination. + $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] ); + + // Query taxes. + $results = $wpdb->get_results( $query . $order_by . $pagination ); + + $taxes = array(); + foreach ( $results as $tax ) { + $data = $this->prepare_item_for_response( $tax, $request ); + $taxes[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $taxes ); + + // Store pagation values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + // Query only for ids. + $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); + + // Calcule totals. + $total_taxes = (int) $wpdb->num_rows; + $response->header( 'X-WP-Total', (int) $total_taxes ); + $max_pages = ceil( $total_taxes / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Take tax data from the request and return the updated or newly created rate. + * + * @param WP_REST_Request $request Full details about the request. + * @param stdClass|null $current Existing tax object. + * @return object + */ + protected function create_or_update_tax( $request, $current = null ) { + $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); + $data = array(); + $fields = array( + 'tax_rate_country', + 'tax_rate_state', + 'tax_rate', + 'tax_rate_name', + 'tax_rate_priority', + 'tax_rate_compound', + 'tax_rate_shipping', + 'tax_rate_order', + 'tax_rate_class', + ); + + foreach ( $fields as $field ) { + // Keys via API differ from the stored names returned by _get_tax_rate. + $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); + + // Remove data that was not posted. + if ( ! isset( $request[ $key ] ) ) { + continue; + } + + // Test new data against current data. + if ( $current && $current->$field === $request[ $key ] ) { + continue; + } + + // Add to data array. + switch ( $key ) { + case 'tax_rate_priority' : + case 'tax_rate_compound' : + case 'tax_rate_shipping' : + case 'tax_rate_order' : + $data[ $field ] = absint( $request[ $key ] ); + break; + case 'tax_rate_class' : + $data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : ''; + break; + default : + $data[ $field ] = wc_clean( $request[ $key ] ); + break; + } + } + + if ( $id ) { + WC_Tax::_update_tax_rate( $id, $data ); + } else { + $id = WC_Tax::_insert_tax_rate( $data ); + } + + // Add locales. + if ( ! empty( $request['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); + } + if ( ! empty( $request['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); + } + + return WC_Tax::_get_tax_rate( $id, OBJECT ); + } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $tax = $this->create_or_update_tax( $request ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) ); + + return $response; + } + + /** + * Get a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->prepare_item_for_response( $tax_obj, $request ); + $response = rest_ensure_response( $tax ); + + return $response; + } + + /** + * Update a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->create_or_update_tax( $request, $tax_obj ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + + return $response; + } + + /** + * Delete a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a tax is deleted via the REST API. + * + * @param stdClass $tax The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); + + return $response; + } + + /** + * Prepare a single tax output for response. + * + * @param stdClass $tax Tax object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax, $request ) { + global $wpdb; + + $id = (int) $tax->tax_rate_id; + $data = array( + 'id' => $id, + 'country' => $tax->tax_rate_country, + 'state' => $tax->tax_rate_state, + 'postcode' => '', + 'city' => '', + 'rate' => $tax->tax_rate, + 'name' => $tax->tax_rate_name, + 'priority' => (int) $tax->tax_rate_priority, + 'compound' => (bool) $tax->tax_rate_compound, + 'shipping' => (bool) $tax->tax_rate_shipping, + 'order' => (int) $tax->tax_rate_order, + 'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard', + ); + + // Get locales from a tax rate. + $locales = $wpdb->get_results( $wpdb->prepare( " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", $id ) ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $data[ $locale->location_type ] = $locale->location_code; + } + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $tax ) ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $tax Tax object. + * @return array Links for the given tax. + */ + protected function prepare_links( $tax ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Taxes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'country' => array( + 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'State code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postcode / ZIP.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'rate' => array( + 'description' => __( 'Tax rate.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tax rate name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'priority' => array( + 'description' => __( 'Tax priority.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + ), + 'compound' => array( + 'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'shipping' => array( + 'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'order' => array( + 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'standard', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param(); + $params['context']['default'] = 'view'; + + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'order', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'order', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['class'] = array( + 'description' => __( 'Sort by tax class.', 'woocommerce' ), + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_title', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-webhook-deliveries.php b/includes/api/v1/class-wc-rest-webhook-deliveries.php new file mode 100644 index 00000000000..a6c80ccc33b --- /dev/null +++ b/includes/api/v1/class-wc-rest-webhook-deliveries.php @@ -0,0 +1,323 @@ +/deliveries endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Webhook Deliveries controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Controller + */ +class WC_REST_Webhook_Deliveries_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'webhooks/(?P[\d]+)/deliveries'; + + /** + * Register the routes for webhook deliveries. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'webhook_id' => array( + 'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'webhook_id' => array( + 'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'shop_webhook', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a webhook develivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['webhook_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'shop_webhook', 'read', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all webhook deliveries. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $webhook = new WC_Webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $logs = $webhook->get_delivery_logs(); + + $data = array(); + foreach ( $logs as $log ) { + $delivery = $this->prepare_item_for_response( (object) $log, $request ); + $delivery = $this->prepare_response_for_collection( $delivery ); + $data[] = $delivery; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single webhook delivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $webhook = new WC_Webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $log = $webhook->get_delivery_log( $id ); + + if ( empty( $id ) || empty( $log ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( (object) $log, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + /** + * Prepare a single webhook delivery output for response. + * + * @param stdClass $log Delivery log object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $log, $request ) { + $data = (array) $log; + + // Add timestamp. + $data['date_created'] = wc_rest_prepare_date_response( $log->comment->comment_date_gmt ); + + // Remove comment object. + unset( $data['comment'] ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $log ) ); + + /** + * Filter webhook delivery object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $log Delivery log object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $log Delivery log object. + * @return array Links for the given webhook delivery. + */ + protected function prepare_links( $log ) { + $webhook_id = (int) $log->request_headers['X-WC-Webhook-ID']; + $base = str_replace( '(?P[\d]+)', $webhook_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/webhooks/%d', $this->namespace, $webhook_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook_delivery', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'duration' => array( + 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'summary' => array( + 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_url' => array( + 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_headers' => array( + 'description' => __( 'Request headers.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'request_body' => array( + 'description' => __( 'Request body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_code' => array( + 'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_message' => array( + 'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_headers' => array( + 'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'response_body' => array( + 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-webhooks-controller.php b/includes/api/v1/class-wc-rest-webhooks-controller.php new file mode 100644 index 00000000000..1cbdc5796ba --- /dev/null +++ b/includes/api/v1/class-wc-rest-webhooks-controller.php @@ -0,0 +1,593 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for webhooks. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'topic' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Webhook topic.', 'woocommerce' ), + ), + 'delivery_url' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Webhook delivery URL.', 'woocommerce' ), + ), + 'secret' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Webhook secret.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get the default REST API version. + * + * @since 3.0.0 + * @return string + */ + protected function get_default_api_version() { + return 'wp_api_v2'; + } + + /** + * Create a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + // Validate topic. + if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Validate delivery URL. + if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $post->post_type = $this->post_type; + $post_id = wp_insert_post( $post, true ); + + if ( is_wp_error( $post_id ) ) { + + if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + $post->ID = $post_id; + + $webhook = new WC_Webhook( $post_id ); + + // Set topic. + $webhook->set_topic( $request['topic'] ); + + // Set delivery URL. + $webhook->set_delivery_url( $request['delivery_url'] ); + + // Set secret. + $webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : '' ); + + // Set API version to WP API integration. + $webhook->set_api_version( $this->get_default_api_version() ); + + // Set status. + if ( ! empty( $request['status'] ) ) { + $webhook->update_status( $request['status'] ); + } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); + + // Send ping. + $webhook->deliver_ping(); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return $response; + } + + /** + * Update a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $webhook = new WC_Webhook( $id ); + + // Update topic. + if ( ! empty( $request['topic'] ) ) { + if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { + $webhook->set_topic( $request['topic'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update delivery URL. + if ( ! empty( $request['delivery_url'] ) ) { + if ( wc_is_valid_url( $request['delivery_url'] ) ) { + $webhook->set_delivery_url( $request['delivery_url'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update secret. + if ( ! empty( $request['secret'] ) ) { + $webhook->set_secret( $request['secret'] ); + } + + // Update status. + if ( ! empty( $request['status'] ) ) { + $webhook->update_status( $request['status'] ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. + $post_id = wp_update_post( (array) $post, true ); + if ( is_wp_error( $post_id ) ) { + if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + $result = wp_delete_post( $id, true ); + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param object $post The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return $response; + } + + /** + * Prepare a single webhook for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + $data = new stdClass; + + // Post ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { + // @codingStandardsIgnoreStart + $data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); + // @codingStandardsIgnoreEnd + + // Post author. + $data->post_author = get_current_user_id(); + + // Post password. + $password = strlen( uniqid( 'webhook_' ) ); + $data->post_password = $password > 20 ? substr( $password, 0, 20 ) : $password; + + // Post status. + $data->post_status = 'publish'; + } else { + + // Allow edit post title. + if ( ! empty( $request['name'] ) ) { + $data->post_title = $request['name']; + } + } + + // Comment status. + $data->comment_status = 'closed'; + + // Ping status. + $data->ping_status = 'closed'; + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param stdClass $data An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + + /** + * Prepare a single webhook output for response. + * + * @param object $post + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $post, $request ) { + $id = (int) $post->ID; + $webhook = new WC_Webhook( $id ); + $data = array( + 'id' => $webhook->id, + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'date_created' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $post, $request ) ); + + /** + * Filter webhook object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Webhook $webhook Webhook object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + switch ( $request['status'] ) { + case 'active' : + $args['post_status'] = 'publish'; + break; + case 'paused' : + $args['post_status'] = 'draft'; + break; + case 'disabled' : + $args['post_status'] = 'pending'; + break; + default : + $args['post_status'] = 'any'; + break; + } + + return $args; + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Webhook status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'active', + 'enum' => array( 'active', 'paused', 'disabled' ), + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wc_is_webhook_valid_topic', + ), + ), + 'topic' => array( + 'description' => __( 'Webhook topic.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'resource' => array( + 'description' => __( 'Webhook resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'event' => array( + 'description' => __( 'Webhook event.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'hooks' => array( + 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'delivery_url' => array( + 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'secret' => array( + 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default is a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'all', + 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'all', 'active', 'paused', 'disabled' ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/class-wc-ajax.php b/includes/class-wc-ajax.php index 8fae68c7ad9..4b5b6d13898 100644 --- a/includes/class-wc-ajax.php +++ b/includes/class-wc-ajax.php @@ -1,63 +1,147 @@ hide_errors(); + } + } + + /** + * Send headers for WC Ajax Requests. + * + * @since 2.5.0 + */ + private static function wc_ajax_headers() { + send_origin_headers(); + @header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) ); + @header( 'X-Robots-Tag: noindex' ); + send_nosniff_header(); + nocache_headers(); + status_header( 200 ); + } + + /** + * Check for WC Ajax request and fire action. + */ + public static function do_wc_ajax() { + global $wp_query; + + if ( ! empty( $_GET['wc-ajax'] ) ) { + $wp_query->set( 'wc-ajax', sanitize_text_field( $_GET['wc-ajax'] ) ); + } + + if ( $action = $wp_query->get( 'wc-ajax' ) ) { + self::wc_ajax_headers(); + do_action( 'wc_ajax_' . sanitize_text_field( $action ) ); + wp_die(); + } + } + + /** + * Hook in methods - uses WordPress ajax handlers (admin-ajax). + */ + public static function add_ajax_events() { // woocommerce_EVENT => nopriv $ajax_events = array( - 'get_refreshed_fragments' => true, - 'apply_coupon' => true, - 'update_shipping_method' => true, - 'update_order_review' => true, - 'add_to_cart' => true, - 'checkout' => true, - 'feature_product' => false, - 'mark_order_complete' => false, - 'mark_order_processing' => false, - 'add_new_attribute' => false, - 'remove_variation' => false, - 'remove_variations' => false, - 'save_attributes' => false, - 'add_variation' => false, - 'link_all_variations' => false, - 'revoke_access_to_download' => false, - 'grant_access_to_download' => false, - 'get_customer_details' => false, - 'add_order_item' => false, - 'add_order_fee' => false, - 'remove_order_item' => false, - 'reduce_order_item_stock' => false, - 'increase_order_item_stock' => false, - 'add_order_item_meta' => false, - 'remove_order_item_meta' => false, - 'calc_line_taxes' => false, - 'add_order_note' => false, - 'delete_order_note' => false, - 'json_search_products' => false, - 'json_search_products_and_variations' => false, - 'json_search_downloadable_products_and_variations' => false, - 'json_search_customers' => false, - 'term_ordering' => false, - 'product_ordering' => false + 'get_refreshed_fragments' => true, + 'apply_coupon' => true, + 'remove_coupon' => true, + 'update_shipping_method' => true, + 'get_cart_totals' => true, + 'update_order_review' => true, + 'add_to_cart' => true, + 'checkout' => true, + 'get_variation' => true, + 'get_customer_location' => true, + 'feature_product' => false, + 'mark_order_status' => false, + 'add_attribute' => false, + 'add_new_attribute' => false, + 'remove_variation' => false, + 'remove_variations' => false, + 'save_attributes' => false, + 'add_variation' => false, + 'link_all_variations' => false, + 'revoke_access_to_download' => false, + 'grant_access_to_download' => false, + 'get_customer_details' => false, + 'add_order_item' => false, + 'add_order_fee' => false, + 'add_order_shipping' => false, + 'add_order_tax' => false, + 'remove_order_item' => false, + 'remove_order_tax' => false, + 'reduce_order_item_stock' => false, + 'increase_order_item_stock' => false, + 'add_order_item_meta' => false, + 'remove_order_item_meta' => false, + 'calc_line_taxes' => false, + 'save_order_items' => false, + 'load_order_items' => false, + 'add_order_note' => false, + 'delete_order_note' => false, + 'json_search_products' => false, + 'json_search_products_and_variations' => false, + 'json_search_downloadable_products_and_variations' => false, + 'json_search_customers' => false, + 'term_ordering' => false, + 'product_ordering' => false, + 'refund_line_items' => false, + 'delete_refund' => false, + 'rated' => false, + 'update_api_key' => false, + 'load_variations' => false, + 'save_variations' => false, + 'bulk_edit_variations' => false, + 'tax_rates_save_changes' => false, + 'shipping_zones_save_changes' => false, + 'shipping_zone_add_method' => false, + 'shipping_zone_methods_save_changes' => false, + 'shipping_zone_methods_save_settings' => false, + 'shipping_classes_save_changes' => false, ); foreach ( $ajax_events as $ajax_event => $nopriv ) { @@ -65,39 +149,36 @@ class WC_AJAX { if ( $nopriv ) { add_action( 'wp_ajax_nopriv_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) ); + + // WC AJAX can be used for frontend ajax requests. + add_action( 'wc_ajax_' . $ajax_event, array( __CLASS__, $ajax_event ) ); } } - - add_action( 'wp_ajax_page_slurp', array( 'WC_Gateway_Mijireh', 'page_slurp' ) ); } /** - * Get a refreshed cart fragment + * Get a refreshed cart fragment, including the mini cart HTML. */ public static function get_refreshed_fragments() { - - // Get mini cart ob_start(); woocommerce_mini_cart(); $mini_cart = ob_get_clean(); - // Fragments and mini cart are returned $data = array( - 'fragments' => apply_filters( 'add_to_cart_fragments', array( - 'div.widget_shopping_cart_content' => '
    ' . $mini_cart . '
    ' + 'fragments' => apply_filters( 'woocommerce_add_to_cart_fragments', array( + 'div.widget_shopping_cart_content' => '
    ' . $mini_cart . '
    ', ) ), - 'cart_hash' => WC()->cart->get_cart() ? md5( json_encode( WC()->cart->get_cart() ) ) : '' + 'cart_hash' => apply_filters( 'woocommerce_add_to_cart_hash', WC()->cart->get_cart_for_session() ? md5( json_encode( WC()->cart->get_cart_for_session() ) ) : '', WC()->cart->get_cart_for_session() ), ); wp_send_json( $data ); - } /** - * AJAX apply coupon on checkout page + * AJAX apply coupon on checkout page. */ public static function apply_coupon() { @@ -110,20 +191,35 @@ class WC_AJAX { } wc_print_notices(); - - die(); + wp_die(); } /** - * AJAX update shipping method on cart page + * AJAX remove coupon on cart and checkout page. + */ + public static function remove_coupon() { + check_ajax_referer( 'remove-coupon', 'security' ); + + $coupon = isset( $_POST['coupon'] ) ? wc_clean( $_POST['coupon'] ) : false; + + if ( empty( $coupon ) ) { + wc_add_notice( __( 'Sorry there was a problem removing this coupon.', 'woocommerce' ), 'error' ); + } else { + WC()->cart->remove_coupon( $coupon ); + wc_add_notice( __( 'Coupon has been removed.', 'woocommerce' ) ); + } + + wc_print_notices(); + wp_die(); + } + + /** + * AJAX update shipping method on cart page. */ public static function update_shipping_method() { - check_ajax_referer( 'update-shipping-method', 'security' ); - if ( ! defined('WOOCOMMERCE_CART') ) { - define( 'WOOCOMMERCE_CART', true ); - } + wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); @@ -135,27 +231,40 @@ class WC_AJAX { WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); - WC()->cart->calculate_totals(); - - woocommerce_cart_totals(); - - die(); + self::get_cart_totals(); } /** - * AJAX update order review on checkout + * AJAX receive updated cart_totals div. + */ + public static function get_cart_totals() { + wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); + WC()->cart->calculate_totals(); + woocommerce_cart_totals(); + wp_die(); + } + + /** + * Session has expired. + */ + private static function update_order_review_expired() { + wp_send_json( array( + 'fragments' => apply_filters( 'woocommerce_update_order_review_fragments', array( + 'form.woocommerce-checkout' => '
    ' . __( 'Sorry, your session has expired.', 'woocommerce' ) . ' ' . __( 'Return to shop', 'woocommerce' ) . '
    ', + ) ), + ) ); + } + + /** + * AJAX update order review on checkout. */ public static function update_order_review() { - check_ajax_referer( 'update-order-review', 'security' ); - if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { - define( 'WOOCOMMERCE_CHECKOUT', true ); - } + wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); - if ( 0 == sizeof( WC()->cart->get_cart() ) ) { - echo '
    ' . __( 'Sorry, your session has expired.', 'woocommerce' ) . ' ' . __( 'Return to homepage', 'woocommerce' ) . '
    '; - die(); + if ( WC()->cart->is_empty() ) { + self::update_order_review_expired(); } do_action( 'woocommerce_checkout_update_order_review', $_POST['post_data'] ); @@ -170,104 +279,92 @@ class WC_AJAX { WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); WC()->session->set( 'chosen_payment_method', empty( $_POST['payment_method'] ) ? '' : $_POST['payment_method'] ); - - if ( isset( $_POST['country'] ) ) { - WC()->customer->set_country( $_POST['country'] ); - } - - if ( isset( $_POST['state'] ) ) { - WC()->customer->set_state( $_POST['state'] ); - } - - if ( isset( $_POST['postcode'] ) ) { - WC()->customer->set_postcode( $_POST['postcode'] ); - } - - if ( isset( $_POST['city'] ) ) { - WC()->customer->set_city( $_POST['city'] ); - } - - if ( isset( $_POST['address'] ) ) { - WC()->customer->set_address( $_POST['address'] ); - } - - if ( isset( $_POST['address_2'] ) ) { - WC()->customer->set_address_2( $_POST['address_2'] ); - } + WC()->customer->set_props( array( + 'billing_country' => isset( $_POST['country'] ) ? $_POST['country'] : null, + 'billing_state' => isset( $_POST['state'] ) ? $_POST['state'] : null, + 'billing_postcode' => isset( $_POST['postcode'] ) ? $_POST['postcode'] : null, + 'billing_city' => isset( $_POST['city'] ) ? $_POST['city'] : null, + 'billing_address_1' => isset( $_POST['address'] ) ? $_POST['address'] : null, + 'billing_address_2' => isset( $_POST['address_2'] ) ? $_POST['address_2'] : null, + ) ); if ( wc_ship_to_billing_address_only() ) { - - if ( isset( $_POST['country'] ) ) { - WC()->customer->set_shipping_country( $_POST['country'] ); - } - - if ( isset( $_POST['state'] ) ) { - WC()->customer->set_shipping_state( $_POST['state'] ); - } - - if ( isset( $_POST['postcode'] ) ) { - WC()->customer->set_shipping_postcode( $_POST['postcode'] ); - } - - if ( isset( $_POST['city'] ) ) { - WC()->customer->set_shipping_city( $_POST['city'] ); - } - - if ( isset( $_POST['address'] ) ) { - WC()->customer->set_shipping_address( $_POST['address'] ); - } - - if ( isset( $_POST['address_2'] ) ) { - WC()->customer->set_shipping_address_2( $_POST['address_2'] ); + WC()->customer->set_props( array( + 'shipping_country' => isset( $_POST['country'] ) ? $_POST['country'] : null, + 'shipping_state' => isset( $_POST['state'] ) ? $_POST['state'] : null, + 'shipping_postcode' => isset( $_POST['postcode'] ) ? $_POST['postcode'] : null, + 'shipping_city' => isset( $_POST['city'] ) ? $_POST['city'] : null, + 'shipping_address_1' => isset( $_POST['address'] ) ? $_POST['address'] : null, + 'shipping_address_2' => isset( $_POST['address_2'] ) ? $_POST['address_2'] : null, + ) ); + if ( ! empty( $_POST['country'] ) ) { + WC()->customer->set_calculated_shipping( true ); } } else { - - if ( isset( $_POST['s_country'] ) ) { - WC()->customer->set_shipping_country( $_POST['s_country'] ); - } - - if ( isset( $_POST['s_state'] ) ) { - WC()->customer->set_shipping_state( $_POST['s_state'] ); - } - - if ( isset( $_POST['s_postcode'] ) ) { - WC()->customer->set_shipping_postcode( $_POST['s_postcode'] ); - } - - if ( isset( $_POST['s_city'] ) ) { - WC()->customer->set_shipping_city( $_POST['s_city'] ); - } - - if ( isset( $_POST['s_address'] ) ) { - WC()->customer->set_shipping_address( $_POST['s_address'] ); - } - - if ( isset( $_POST['s_address_2'] ) ) { - WC()->customer->set_shipping_address_2( $_POST['s_address_2'] ); + WC()->customer->set_props( array( + 'shipping_country' => isset( $_POST['s_country'] ) ? $_POST['s_country'] : null, + 'shipping_state' => isset( $_POST['s_state'] ) ? $_POST['s_state'] : null, + 'shipping_postcode' => isset( $_POST['s_postcode'] ) ? $_POST['s_postcode'] : null, + 'shipping_city' => isset( $_POST['s_city'] ) ? $_POST['s_city'] : null, + 'shipping_address_1' => isset( $_POST['s_address'] ) ? $_POST['s_address'] : null, + 'shipping_address_2' => isset( $_POST['s_address_2'] ) ? $_POST['s_address_2'] : null, + ) ); + if ( ! empty( $_POST['s_country'] ) ) { + WC()->customer->set_calculated_shipping( true ); } } + WC()->customer->save(); WC()->cart->calculate_totals(); - do_action( 'woocommerce_checkout_order_review' ); // Display review order table + // Get order review fragment + ob_start(); + woocommerce_order_review(); + $woocommerce_order_review = ob_get_clean(); - die(); + // Get checkout payment fragment + ob_start(); + woocommerce_checkout_payment(); + $woocommerce_checkout_payment = ob_get_clean(); + + // Get messages if reload checkout is not true + $messages = ''; + if ( ! isset( WC()->session->reload_checkout ) ) { + ob_start(); + wc_print_notices(); + $messages = ob_get_clean(); + } + + unset( WC()->session->refresh_totals, WC()->session->reload_checkout ); + + wp_send_json( array( + 'result' => empty( $messages ) ? 'success' : 'failure', + 'messages' => $messages, + 'reload' => isset( WC()->session->reload_checkout ) ? 'true' : 'false', + 'fragments' => apply_filters( 'woocommerce_update_order_review_fragments', array( + '.woocommerce-checkout-review-order-table' => $woocommerce_order_review, + '.woocommerce-checkout-payment' => $woocommerce_checkout_payment, + ) ), + ) ); } /** - * AJAX add to cart + * AJAX add to cart. */ public static function add_to_cart() { + ob_start(); + $product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $_POST['product_id'] ) ); $quantity = empty( $_POST['quantity'] ) ? 1 : wc_stock_amount( $_POST['quantity'] ); $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); + $product_status = get_post_status( $product_id ); - if ( $passed_validation && WC()->cart->add_to_cart( $product_id, $quantity ) ) { + if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity ) && 'publish' === $product_status ) { do_action( 'woocommerce_ajax_added_to_cart', $product_id ); if ( get_option( 'woocommerce_cart_redirect_after_add' ) == 'yes' ) { - wc_add_to_cart_message( $product_id ); + wc_add_to_cart_message( array( $product_id => $quantity ), true ); } // Return fragments @@ -277,568 +374,302 @@ class WC_AJAX { // If there was an error adding to the cart, redirect to the product page to show any errors $data = array( - 'error' => true, - 'product_url' => apply_filters( 'woocommerce_cart_redirect_after_error', get_permalink( $product_id ), $product_id ) + 'error' => true, + 'product_url' => apply_filters( 'woocommerce_cart_redirect_after_error', get_permalink( $product_id ), $product_id ), ); wp_send_json( $data ); - } - - die(); } /** - * Process ajax checkout form + * Process ajax checkout form. */ public static function checkout() { - if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { - define( 'WOOCOMMERCE_CHECKOUT', true ); - } - + wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); WC()->checkout()->process_checkout(); - - die(0); + wp_die( 0 ); } /** - * Feature a product from admin + * Get a matching variation based on posted attributes. + */ + public static function get_variation() { + ob_start(); + + if ( empty( $_POST['product_id'] ) || ! ( $variable_product = wc_get_product( absint( $_POST['product_id'] ) ) ) ) { + wp_die(); + } + + $data_store = WC_Data_Store::load( 'product' ); + $variation_id = $data_store->find_matching_product_variation( $variable_product, wp_unslash( $_POST ) ); + $variation = $variation_id ? $variable_product->get_available_variation( $variation_id ) : false; + wp_send_json( $variation ); + } + + /** + * Locate user via AJAX. + */ + public static function get_customer_location() { + $location_hash = WC_Cache_Helper::geolocation_ajax_get_location_hash(); + wp_send_json_success( array( 'hash' => $location_hash ) ); + } + + /** + * Toggle Featured status of a product from admin. */ public static function feature_product() { - if ( ! current_user_can( 'edit_products' ) ) { - wp_die( __( 'You do not have sufficient permissions to access this page.', 'woocommerce' ), '', array( 'response' => 403 ) ); + if ( current_user_can( 'edit_products' ) && check_admin_referer( 'woocommerce-feature-product' ) ) { + $product = wc_get_product( absint( $_GET['product_id'] ) ); + + if ( $product ) { + $product->set_featured( ! $product->get_featured() ); + $product->save(); + } } - if ( ! check_admin_referer( 'woocommerce-feature-product' ) ) { - wp_die( __( 'You have taken too long. Please go back and retry.', 'woocommerce' ), '', array( 'response' => 403 ) ); - } - - $post_id = ! empty( $_GET['product_id'] ) ? (int) $_GET['product_id'] : ''; - - if ( ! $post_id || get_post_type( $post_id ) !== 'product' ) { - die; - } - - $featured = get_post_meta( $post_id, '_featured', true ); - - if ( 'yes' === $featured ) { - update_post_meta( $post_id, '_featured', 'no' ); - } else { - update_post_meta( $post_id, '_featured', 'yes' ); - } - - delete_transient( 'wc_featured_products' ); - - wp_safe_redirect( remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids' ), wp_get_referer() ) ); - - die(); + wp_safe_redirect( wp_get_referer() ? remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids' ), wp_get_referer() ) : admin_url( 'edit.php?post_type=product' ) ); + exit; } /** - * Mark an order as complete + * Mark an order with a status. */ - public static function mark_order_complete() { - if ( ! current_user_can( 'edit_shop_orders' ) ) { - wp_die( __( 'You do not have sufficient permissions to access this page.', 'woocommerce' ), '', array( 'response' => 403 ) ); + public static function mark_order_status() { + if ( current_user_can( 'edit_shop_orders' ) && check_admin_referer( 'woocommerce-mark-order-status' ) ) { + $status = sanitize_text_field( $_GET['status'] ); + $order = wc_get_order( absint( $_GET['order_id'] ) ); + + if ( wc_is_order_status( 'wc-' . $status ) && $order ) { + $order->update_status( $status, '', true ); + do_action( 'woocommerce_order_edit_status', $order->get_id(), $status ); + } } - if ( ! check_admin_referer( 'woocommerce-mark-order-complete' ) ) { - wp_die( __( 'You have taken too long. Please go back and retry.', 'woocommerce' ), '', array( 'response' => 403 ) ); - } - - $order_id = isset( $_GET['order_id'] ) && (int) $_GET['order_id'] ? (int) $_GET['order_id'] : ''; - if ( ! $order_id ) { - die(); - } - - $order = get_order( $order_id ); - $order->update_status( 'completed' ); - - wp_safe_redirect( wp_get_referer() ); - - die(); + wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php?post_type=shop_order' ) ); + exit; } /** - * Mark an order as processing + * Add an attribute row. */ - public static function mark_order_processing() { - if ( ! current_user_can( 'edit_shop_orders' ) ) { - wp_die( __( 'You do not have sufficient permissions to access this page.', 'woocommerce' ), '', array( 'response' => 403 ) ); - } - - if ( ! check_admin_referer( 'woocommerce-mark-order-processing' ) ) { - wp_die( __( 'You have taken too long. Please go back and retry.', 'woocommerce' ), '', array( 'response' => 403 ) ); - } - - $order_id = isset( $_GET['order_id'] ) && (int) $_GET['order_id'] ? (int) $_GET['order_id'] : ''; - if ( ! $order_id ) { - die(); - } - - $order = get_order( $order_id ); - $order->update_status( 'processing' ); - - wp_safe_redirect( wp_get_referer() ); - - die(); - } - - /** - * Add a new attribute via ajax function - */ - public static function add_new_attribute() { + public static function add_attribute() { + ob_start(); check_ajax_referer( 'add-attribute', 'security' ); - $taxonomy = esc_attr( $_POST['taxonomy'] ); - $term = stripslashes( $_POST['term'] ); + if ( ! current_user_can( 'edit_products' ) ) { + wp_die( -1 ); + } - if ( taxonomy_exists( $taxonomy ) ) { + $i = absint( $_POST['i'] ); + $metabox_class = array(); + $attribute = new WC_Product_Attribute(); - $result = wp_insert_term( $term, $taxonomy ); + $attribute->set_id( wc_attribute_taxonomy_id_by_name( sanitize_text_field( $_POST['taxonomy'] ) ) ); + $attribute->set_name( sanitize_text_field( $_POST['taxonomy'] ) ); + $attribute->set_visible( apply_filters( 'woocommerce_attribute_default_visibility', 1 ) ); + $attribute->set_variation( apply_filters( 'woocommerce_attribute_default_is_variation', 0 ) ); - if ( is_wp_error( $result ) ) { - wp_send_json( array( - 'error' => $result->get_error_message() - ) ); - } else { - wp_send_json( array( - 'term_id' => $result['term_id'], - 'name' => $term, - 'slug' => sanitize_title( $term ), - ) ); + if ( $attribute->is_taxonomy() ) { + $metabox_class[] = 'taxonomy'; + $metabox_class[] = $attribute->get_name(); + } + + include( 'admin/meta-boxes/views/html-product-attribute.php' ); + wp_die(); + } + + /** + * Add a new attribute via ajax function. + */ + public static function add_new_attribute() { + check_ajax_referer( 'add-attribute', 'security' ); + + if ( current_user_can( 'manage_product_terms' ) ) { + $taxonomy = esc_attr( $_POST['taxonomy'] ); + $term = wc_clean( $_POST['term'] ); + + if ( taxonomy_exists( $taxonomy ) ) { + + $result = wp_insert_term( $term, $taxonomy ); + + if ( is_wp_error( $result ) ) { + wp_send_json( array( + 'error' => $result->get_error_message(), + ) ); + } else { + $term = get_term_by( 'id', $result['term_id'], $taxonomy ); + wp_send_json( array( + 'term_id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ) ); + } } } - - die(); + wp_die( -1 ); } /** - * Delete variation via ajax function - */ - public static function remove_variation() { - - check_ajax_referer( 'delete-variation', 'security' ); - - $variation_id = intval( $_POST['variation_id'] ); - $variation = get_post( $variation_id ); - - if ( $variation && 'product_variation' == $variation->post_type ) { - wp_delete_post( $variation_id ); - } - - die(); - } - - /** - * Delete variations via ajax function + * Delete variations via ajax function. */ public static function remove_variations() { - check_ajax_referer( 'delete-variations', 'security' ); - $variation_ids = (array) $_POST['variation_ids']; + if ( current_user_can( 'edit_products' ) ) { + $variation_ids = (array) $_POST['variation_ids']; - foreach ( $variation_ids as $variation_id ) { - $variation = get_post( $variation_id ); - - if ( $variation && 'product_variation' == $variation->post_type ) { - wp_delete_post( $variation_id ); + foreach ( $variation_ids as $variation_id ) { + if ( 'product_variation' === get_post_type( $variation_id ) ) { + $variation = wc_get_product( $variation_id ); + $variation->delete( true ); + } } } - die(); + wp_die( -1 ); } /** - * Save attributes via ajax + * Save attributes via ajax. */ public static function save_attributes() { - check_ajax_referer( 'save-attributes', 'security' ); - // Get post data + if ( ! current_user_can( 'edit_products' ) ) { + wp_die( -1 ); + } + parse_str( $_POST['data'], $data ); - $post_id = absint( $_POST['post_id'] ); - // Save Attributes - $attributes = array(); + $attributes = WC_Meta_Box_Product_Data::prepare_attributes( $data ); + $product_id = absint( $_POST['post_id'] ); + $product_type = ! empty( $_POST['product_type'] ) ? wc_clean( $_POST['product_type'] ) : 'simple'; + $classname = WC_Product_Factory::get_product_classname( $product_id, $product_type ); + $product = new $classname( $product_id ); - if ( isset( $data['attribute_names'] ) ) { - - $attribute_names = array_map( 'stripslashes', $data['attribute_names'] ); - $attribute_values = isset( $data['attribute_values'] ) ? $data['attribute_values'] : array(); - - if ( isset( $data['attribute_visibility'] ) ) { - $attribute_visibility = $data['attribute_visibility']; - } - - if ( isset( $data['attribute_variation'] ) ) { - $attribute_variation = $data['attribute_variation']; - } - - $attribute_is_taxonomy = $data['attribute_is_taxonomy']; - $attribute_position = $data['attribute_position']; - $attribute_names_count = sizeof( $attribute_names ); - - for ( $i = 0; $i < $attribute_names_count; $i++ ) { - if ( ! $attribute_names[ $i ] ) { - continue; - } - - $is_visible = isset( $attribute_visibility[ $i ] ) ? 1 : 0; - $is_variation = isset( $attribute_variation[ $i ] ) ? 1 : 0; - $is_taxonomy = $attribute_is_taxonomy[ $i ] ? 1 : 0; - - if ( $is_taxonomy ) { - - if ( isset( $attribute_values[ $i ] ) ) { - - // Select based attributes - Format values (posted values are slugs) - if ( is_array( $attribute_values[ $i ] ) ) { - $values = array_map( 'sanitize_title', $attribute_values[ $i ] ); - - // Text based attributes - Posted values are term names - don't change to slugs - } else { - $values = array_map( 'stripslashes', array_map( 'strip_tags', explode( WC_DELIMITER, $attribute_values[ $i ] ) ) ); - } - - // Remove empty items in the array - $values = array_filter( $values, 'strlen' ); - - } else { - $values = array(); - } - - // Update post terms - if ( taxonomy_exists( $attribute_names[ $i ] ) ) { - wp_set_object_terms( $post_id, $values, $attribute_names[ $i ] ); - } - - if ( $values ) { - // Add attribute to array, but don't set values - $attributes[ sanitize_title( $attribute_names[ $i ] ) ] = array( - 'name' => wc_clean( $attribute_names[ $i ] ), - 'value' => '', - 'position' => $attribute_position[ $i ], - 'is_visible' => $is_visible, - 'is_variation' => $is_variation, - 'is_taxonomy' => $is_taxonomy - ); - } - - } elseif ( isset( $attribute_values[ $i ] ) ) { - - // Text based, separate by pipe - $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', array_map( 'stripslashes', explode( WC_DELIMITER, $attribute_values[ $i ] ) ) ) ); - - // Custom attribute - Add attribute to array and set the values - $attributes[ sanitize_title( $attribute_names[ $i ] ) ] = array( - 'name' => wc_clean( $attribute_names[ $i ] ), - 'value' => $values, - 'position' => $attribute_position[ $i ], - 'is_visible' => $is_visible, - 'is_variation' => $is_variation, - 'is_taxonomy' => $is_taxonomy - ); - } - - } - } - - if ( ! function_exists( 'attributes_cmp' ) ) { - function attributes_cmp( $a, $b ) { - if ( $a['position'] == $b['position'] ) { - return 0; - } - - return ( $a['position'] < $b['position'] ) ? -1 : 1; - } - } - uasort( $attributes, 'attributes_cmp' ); - - update_post_meta( $post_id, '_product_attributes', $attributes ); - - die(); + $product->set_attributes( $attributes ); + $product->save(); + wp_die(); } /** - * Add variation via ajax function + * Add variation via ajax function. */ public static function add_variation() { check_ajax_referer( 'add-variation', 'security' ); - $post_id = intval( $_POST['post_id'] ); - $loop = intval( $_POST['loop'] ); - - $variation = array( - 'post_title' => 'Product #' . $post_id . ' Variation', - 'post_content' => '', - 'post_status' => 'publish', - 'post_author' => get_current_user_id(), - 'post_parent' => $post_id, - 'post_type' => 'product_variation' - ); - - $variation_id = wp_insert_post( $variation ); - - do_action( 'woocommerce_create_product_variation', $variation_id ); - - if ( $variation_id ) { - - $variation_post_status = 'publish'; - $variation_data = get_post_meta( $variation_id ); - $variation_data['variation_post_id'] = $variation_id; - - // Get attributes - $attributes = (array) maybe_unserialize( get_post_meta( $post_id, '_product_attributes', true ) ); - - // Get tax classes - $tax_classes = array_filter(array_map('trim', explode("\n", get_option('woocommerce_tax_classes')))); - $tax_class_options = array(); - $tax_class_options['parent'] =__( 'Same as parent', 'woocommerce' ); - $tax_class_options[''] = __( 'Standard', 'woocommerce' ); - - if ( $tax_classes ) { - foreach ( $tax_classes as $class ) { - $tax_class_options[ sanitize_title( $class ) ] = $class; - } - } - - // Get parent data - $parent_data = array( - 'id' => $post_id, - 'attributes' => $attributes, - 'tax_class_options' => $tax_class_options, - 'sku' => get_post_meta( $post_id, '_sku', true ), - 'weight' => get_post_meta( $post_id, '_weight', true ), - 'length' => get_post_meta( $post_id, '_length', true ), - 'width' => get_post_meta( $post_id, '_width', true ), - 'height' => get_post_meta( $post_id, '_height', true ), - 'tax_class' => get_post_meta( $post_id, '_tax_class', true ) - ); - - if ( ! $parent_data['weight'] ) { - $parent_data['weight'] = '0.00'; - } - - if ( ! $parent_data['length'] ) { - $parent_data['length'] = '0'; - } - - if ( ! $parent_data['width'] ) { - $parent_data['width'] = '0'; - } - - if ( ! $parent_data['height'] ) { - $parent_data['height'] = '0'; - } - - $_tax_class = ''; - $_downloadable_files = ''; - $image_id = 0; - $variation = get_post( $variation_id ); // Get the variation object - - include( 'admin/meta-boxes/views/html-variation-admin.php' ); + if ( ! current_user_can( 'edit_products' ) ) { + wp_die( -1 ); } - die(); + global $post; // Set $post global so its available, like within the admin screens + + $product_id = intval( $_POST['post_id'] ); + $post = get_post( $product_id ); + $loop = intval( $_POST['loop'] ); + $product_object = wc_get_product( $product_id ); + $variation_object = new WC_Product_Variation(); + $variation_object->set_parent_id( $product_id ); + $variation_id = $variation_object->save(); + $variation = get_post( $variation_id ); + $variation_data = array_merge( array_map( 'maybe_unserialize', get_post_custom( $variation_id ) ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compat. + include( 'admin/meta-boxes/views/html-variation-admin.php' ); + wp_die(); } /** - * Link all variations via ajax function + * Link all variations via ajax function. */ public static function link_all_variations() { - - if ( ! defined( 'WC_MAX_LINKED_VARIATIONS' ) ) { - define( 'WC_MAX_LINKED_VARIATIONS', 49 ); - } - check_ajax_referer( 'link-variations', 'security' ); - @set_time_limit(0); + if ( ! current_user_can( 'edit_products' ) ) { + wp_die( -1 ); + } + + wc_maybe_define_constant( 'WC_MAX_LINKED_VARIATIONS', 49 ); + wc_set_time_limit( 0 ); $post_id = intval( $_POST['post_id'] ); if ( ! $post_id ) { - die(); + wp_die(); } $variations = array(); - $_product = get_product( $post_id, array( 'product_type' => 'variable' ) ); + $product = wc_get_product( $post_id ); + $attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' ); - // Put variation attributes into an array - foreach ( $_product->get_attributes() as $attribute ) { + if ( ! empty( $attributes ) ) { + // Get existing variations so we don't create duplicates. + $existing_variations = array_map( 'wc_get_product', $product->get_children() ); + $existing_attributes = array(); - if ( ! $attribute['is_variation'] ) { - continue; + foreach ( $existing_variations as $existing_variation ) { + $existing_attributes[] = $existing_variation->get_attributes(); } - $attribute_field_name = 'attribute_' . sanitize_title( $attribute['name'] ); + $added = 0; + $possible_attributes = wc_array_cartesian( $attributes ); - if ( $attribute['is_taxonomy'] ) { - $options = wc_get_product_terms( $post_id, $attribute['name'], array( 'fields' => 'names' ) ); - } else { - $options = explode( WC_DELIMITER, $attribute['value'] ); - } - - $options = array_map( 'sanitize_title', array_map( 'trim', $options ) ); - - $variations[ $attribute_field_name ] = $options; - } - - // Quit out if none were found - if ( sizeof( $variations ) == 0 ) { - die(); - } - - // Get existing variations so we don't create duplicates - $available_variations = array(); - - foreach( $_product->get_children() as $child_id ) { - $child = $_product->get_child( $child_id ); - - if ( ! empty( $child->variation_id ) ) { - $available_variations[] = $child->get_variation_attributes(); - } - } - - // Created posts will all have the following data - $variation_post_data = array( - 'post_title' => 'Product #' . $post_id . ' Variation', - 'post_content' => '', - 'post_status' => 'publish', - 'post_author' => get_current_user_id(), - 'post_parent' => $post_id, - 'post_type' => 'product_variation' - ); - - // Now find all combinations and create posts - if ( ! function_exists( 'array_cartesian' ) ) { - - /** - * @param array $input - * @return array - */ - function array_cartesian( $input ) { - $result = array(); - - while ( list( $key, $values ) = each( $input ) ) { - // If a sub-array is empty, it doesn't affect the cartesian product - if ( empty( $values ) ) { - continue; - } - - // Special case: seeding the product array with the values from the first sub-array - if ( empty( $result ) ) { - foreach ( $values as $value ) { - $result[] = array( $key => $value ); - } - } - else { - // Second and subsequent input sub-arrays work like this: - // 1. In each existing array inside $product, add an item with - // key == $key and value == first item in input sub-array - // 2. Then, for each remaining item in current input sub-array, - // add a copy of each existing array inside $product with - // key == $key and value == first item in current input sub-array - - // Store all items to be added to $product here; adding them on the spot - // inside the foreach will result in an infinite loop - $append = array(); - foreach ( $result as &$product ) { - // Do step 1 above. array_shift is not the most efficient, but it - // allows us to iterate over the rest of the items with a simple - // foreach, making the code short and familiar. - $product[ $key ] = array_shift( $values ); - - // $product is by reference (that's why the key we added above - // will appear in the end result), so make a copy of it here - $copy = $product; - - // Do step 2 above. - foreach ( $values as $item ) { - $copy[ $key ] = $item; - $append[] = $copy; - } - - // Undo the side effecst of array_shift - array_unshift( $values, $product[ $key ] ); - } - - // Out of the foreach, we can add to $results now - $result = array_merge( $result, $append ); - } + foreach ( $possible_attributes as $possible_attribute ) { + if ( in_array( $possible_attribute, $existing_attributes ) ) { + continue; } + $variation = new WC_Product_Variation(); + $variation->set_parent_id( $post_id ); + $variation->set_attributes( $possible_attribute ); - return $result; + do_action( 'product_variation_linked', $variation->save() ); + + if ( ( $added ++ ) > WC_MAX_LINKED_VARIATIONS ) { + break; + } } + + echo $added; } - $variation_ids = array(); - $added = 0; - $possible_variations = array_cartesian( $variations ); - - foreach ( $possible_variations as $variation ) { - - // Check if variation already exists - if ( in_array( $variation, $available_variations ) ) { - continue; - } - - $variation_id = wp_insert_post( $variation_post_data ); - - $variation_ids[] = $variation_id; - - foreach ( $variation as $key => $value ) { - update_post_meta( $variation_id, $key, $value ); - } - - $added++; - - do_action( 'product_variation_linked', $variation_id ); - - if ( $added > WC_MAX_LINKED_VARIATIONS ) { - break; - } - } - - delete_transient( 'wc_product_children_ids_' . $post_id ); - - echo $added; - - die(); + $data_store = $product->get_data_store(); + $data_store->sort_all_product_variations( $product->get_id() ); + wp_die(); } /** - * Delete download permissions via ajax function + * Delete download permissions via ajax function. */ public static function revoke_access_to_download() { - check_ajax_referer( 'revoke-access', 'security' ); - global $wpdb; + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + $download_id = $_POST['download_id']; + $product_id = intval( $_POST['product_id'] ); + $order_id = intval( $_POST['order_id'] ); + $permission_id = absint( $_POST['permission_id'] ); + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->delete_by_id( $permission_id ); - $download_id = $_POST['download_id']; - $product_id = intval( $_POST['product_id'] ); - $order_id = intval( $_POST['order_id'] ); + do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id, $permission_id ); - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE order_id = %d AND product_id = %d AND download_id = %s;", $order_id, $product_id, $download_id ) ); - - do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id ); - - die(); + wp_die(); } /** - * Grant download permissions via ajax function + * Grant download permissions via ajax function. */ public static function grant_access_to_download() { check_ajax_referer( 'grant-access', 'security' ); + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + global $wpdb; $wpdb->hide_errors(); @@ -847,32 +678,29 @@ class WC_AJAX { $product_ids = $_POST['product_ids']; $loop = intval( $_POST['loop'] ); $file_counter = 0; - $order = get_order( $order_id ); + $order = wc_get_order( $order_id ); if ( ! is_array( $product_ids ) ) { $product_ids = array( $product_ids ); } foreach ( $product_ids as $product_id ) { - $product = get_product( $product_id ); - $files = $product->get_files(); + $product = wc_get_product( $product_id ); + $files = $product->get_downloads(); - if ( ! $order->billing_email ) { - die(); + if ( ! $order->get_billing_email() ) { + wp_die(); } - if ( $files ) { + if ( ! empty( $files ) ) { foreach ( $files as $download_id => $file ) { if ( $inserted_id = wc_downloadable_file_permission( $download_id, $product_id, $order ) ) { - - // insert complete - get inserted data - $download = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $inserted_id ) ); - + $download = new WC_Customer_Download( $inserted_id ); $loop ++; $file_counter ++; - if ( isset( $file['name'] ) ) { - $file_count = $file['name']; + if ( $file->get_name() ) { + $file_count = $file->get_name(); } else { $file_count = sprintf( __( 'File %d', 'woocommerce' ), $file_counter ); } @@ -881,412 +709,391 @@ class WC_AJAX { } } } - - die(); + wp_die(); } /** - * Get customer details via ajax + * Get customer details via ajax. */ public static function get_customer_details() { - check_ajax_referer( 'get-customer-details', 'security' ); - $user_id = (int) trim(stripslashes($_POST['user_id'])); - $type_to_load = esc_attr(trim(stripslashes($_POST['type_to_load']))); + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } - $customer_data = array( - $type_to_load . '_first_name' => get_user_meta( $user_id, $type_to_load . '_first_name', true ), - $type_to_load . '_last_name' => get_user_meta( $user_id, $type_to_load . '_last_name', true ), - $type_to_load . '_company' => get_user_meta( $user_id, $type_to_load . '_company', true ), - $type_to_load . '_address_1' => get_user_meta( $user_id, $type_to_load . '_address_1', true ), - $type_to_load . '_address_2' => get_user_meta( $user_id, $type_to_load . '_address_2', true ), - $type_to_load . '_city' => get_user_meta( $user_id, $type_to_load . '_city', true ), - $type_to_load . '_postcode' => get_user_meta( $user_id, $type_to_load . '_postcode', true ), - $type_to_load . '_country' => get_user_meta( $user_id, $type_to_load . '_country', true ), - $type_to_load . '_state' => get_user_meta( $user_id, $type_to_load . '_state', true ), - $type_to_load . '_email' => get_user_meta( $user_id, $type_to_load . '_email', true ), - $type_to_load . '_phone' => get_user_meta( $user_id, $type_to_load . '_phone', true ), - ); + $user_id = absint( $_POST['user_id'] ); + $customer = new WC_Customer( $user_id ); - $customer_data = apply_filters( 'woocommerce_found_customer_details', $customer_data ); + if ( has_filter( 'woocommerce_found_customer_details' ) ) { + wc_deprecated_function( 'The woocommerce_found_customer_details filter', '3.0', 'woocommerce_ajax_get_customer_details' ); + } + $data = $customer->get_data(); + $data['date_created'] = $data['date_created'] ? $data['date_created']->getTimestamp() : null; + $data['date_modified'] = $data['date_modified'] ? $data['date_modified']->getTimestamp() : null; + + $customer_data = apply_filters( 'woocommerce_ajax_get_customer_details', $data, $customer, $user_id ); wp_send_json( $customer_data ); - } /** - * Add order item via ajax + * Add order item via ajax. */ public static function add_order_item() { check_ajax_referer( 'order-item', 'security' ); - $item_to_add = sanitize_text_field( $_POST['item_to_add'] ); - $order_id = absint( $_POST['order_id'] ); - - // Find the item - if ( ! is_numeric( $item_to_add ) ) { - die(); + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); } - $post = get_post( $item_to_add ); + try { + $order_id = absint( $_POST['order_id'] ); + $order = wc_get_order( $order_id ); + $items_to_add = wp_parse_id_list( is_array( $_POST['item_to_add'] ) ? $_POST['item_to_add'] : array( $_POST['item_to_add'] ) ); - if ( ! $post || ( 'product' !== $post->post_type && 'product_variation' !== $post->post_type ) ) { - die(); - } - - $_product = get_product( $post->ID ); - $order = get_order( $order_id ); - $class = 'new_row'; - - // Set values - $item = array(); - - $item['product_id'] = $_product->id; - $item['variation_id'] = isset( $_product->variation_id ) ? $_product->variation_id : ''; - $item['variation_data'] = isset( $_product->variation_data ) ? $_product->variation_data : ''; - $item['name'] = $_product->get_title(); - $item['tax_class'] = $_product->get_tax_class(); - $item['qty'] = 1; - $item['line_subtotal'] = wc_format_decimal( $_product->get_price_excluding_tax() ); - $item['line_subtotal_tax'] = ''; - $item['line_total'] = wc_format_decimal( $_product->get_price_excluding_tax() ); - $item['line_tax'] = ''; - - // Add line item - $item_id = wc_add_order_item( $order_id, array( - 'order_item_name' => $item['name'], - 'order_item_type' => 'line_item' - ) ); - - // Add line item meta - if ( $item_id ) { - wc_add_order_item_meta( $item_id, '_qty', $item['qty'] ); - wc_add_order_item_meta( $item_id, '_tax_class', $item['tax_class'] ); - wc_add_order_item_meta( $item_id, '_product_id', $item['product_id'] ); - wc_add_order_item_meta( $item_id, '_variation_id', $item['variation_id'] ); - wc_add_order_item_meta( $item_id, '_line_subtotal', $item['line_subtotal'] ); - wc_add_order_item_meta( $item_id, '_line_subtotal_tax', $item['line_subtotal_tax'] ); - wc_add_order_item_meta( $item_id, '_line_total', $item['line_total'] ); - wc_add_order_item_meta( $item_id, '_line_tax', $item['line_tax'] ); - - // Store variation data in meta - if ( $item['variation_data'] && is_array( $item['variation_data'] ) ) { - foreach ( $item['variation_data'] as $key => $value ) { - wc_add_order_item_meta( $item_id, str_replace( 'attribute_', '', $key ), $value ); - } + if ( ! $order ) { + throw new Exception( __( 'Invalid order', 'woocommerce' ) ); } - do_action( 'woocommerce_ajax_add_order_item_meta', $item_id, $item ); + ob_start(); + + foreach ( $items_to_add as $item_to_add ) { + if ( ! in_array( get_post_type( $item_to_add ), array( 'product', 'product_variation' ) ) ) { + continue; + } + $item_id = $order->add_product( wc_get_product( $item_to_add ) ); + $item = apply_filters( 'woocommerce_ajax_order_item', $order->get_item( $item_id ), $item_id ); + $order_taxes = $order->get_taxes(); + $class = 'new_row'; + + do_action( 'woocommerce_ajax_add_order_item_meta', $item_id, $item ); + include( 'admin/meta-boxes/views/html-order-item.php' ); + } + + wp_send_json_success( array( + 'html' => ob_get_clean(), + ) ); + } catch ( Exception $e ) { + wp_send_json_error( array( 'error' => $e->getMessage() ) ); } - - $item = apply_filters( 'woocommerce_ajax_order_item', $item, $item_id ); - - include( 'admin/meta-boxes/views/html-order-item.php' ); - - // Quit out - die(); } /** - * Add order fee via ajax + * Add order fee via ajax. */ public static function add_order_fee() { - check_ajax_referer( 'order-item', 'security' ); - $order_id = absint( $_POST['order_id'] ); - $order = get_order( $order_id ); - - // Add line item - $item_id = wc_add_order_item( $order_id, array( - 'order_item_name' => '', - 'order_item_type' => 'fee' - ) ); - - // Add line item meta - if ( $item_id ) { - wc_add_order_item_meta( $item_id, '_tax_class', '' ); - wc_add_order_item_meta( $item_id, '_line_total', '' ); - wc_add_order_item_meta( $item_id, '_line_tax', '' ); + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); } - include( 'admin/meta-boxes/views/html-order-fee.php' ); + try { + $order_id = absint( $_POST['order_id'] ); + $order = wc_get_order( $order_id ); + $order_taxes = $order->get_taxes(); + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order_id ); + $item_id = $item->save(); - // Quit out - die(); + ob_start(); + include( 'admin/meta-boxes/views/html-order-fee.php' ); + + wp_send_json_success( array( + 'html' => ob_get_clean(), + ) ); + } catch ( Exception $e ) { + wp_send_json_error( array( 'error' => $e->getMessage() ) ); + } } /** - * Remove an order item + * Add order shipping cost via ajax. + */ + public static function add_order_shipping() { + check_ajax_referer( 'order-item', 'security' ); + + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + try { + $order_id = absint( $_POST['order_id'] ); + $order = wc_get_order( $order_id ); + $order_taxes = $order->get_taxes(); + $shipping_methods = WC()->shipping() ? WC()->shipping->load_shipping_methods() : array(); + + // Add new shipping + $item = new WC_Order_Item_Shipping(); + $item->set_shipping_rate( new WC_Shipping_Rate() ); + $item->set_order_id( $order_id ); + $item_id = $item->save(); + + ob_start(); + include( 'admin/meta-boxes/views/html-order-shipping.php' ); + + wp_send_json_success( array( + 'html' => ob_get_clean(), + ) ); + } catch ( Exception $e ) { + wp_send_json_error( array( 'error' => $e->getMessage() ) ); + } + } + + /** + * Add order tax column via ajax. + */ + public static function add_order_tax() { + check_ajax_referer( 'order-item', 'security' ); + + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + try { + $order_id = absint( $_POST['order_id'] ); + $rate_id = absint( $_POST['rate_id'] ); + $order = wc_get_order( $order_id ); + $data = get_post_meta( $order_id ); + + // Add new tax + $item = new WC_Order_Item_Tax(); + $item->set_rate( $rate_id ); + $item->set_order_id( $order_id ); + $item->save(); + + ob_start(); + include( 'admin/meta-boxes/views/html-order-items.php' ); + + wp_send_json_success( array( + 'html' => ob_get_clean(), + ) ); + } catch ( Exception $e ) { + wp_send_json_error( array( 'error' => $e->getMessage() ) ); + } + } + + /** + * Remove an order item. */ public static function remove_order_item() { check_ajax_referer( 'order-item', 'security' ); + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + $order_item_ids = $_POST['order_item_ids']; + if ( ! is_array( $order_item_ids ) && is_numeric( $order_item_ids ) ) { + $order_item_ids = array( $order_item_ids ); + } + if ( sizeof( $order_item_ids ) > 0 ) { - foreach( $order_item_ids as $id ) { + foreach ( $order_item_ids as $id ) { wc_delete_order_item( absint( $id ) ); } } - - die(); + wp_die(); } /** - * Reduce order item stock + * Remove an order tax. + */ + public static function remove_order_tax() { + check_ajax_referer( 'order-item', 'security' ); + + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + $order_id = absint( $_POST['order_id'] ); + $rate_id = absint( $_POST['rate_id'] ); + + wc_delete_order_item( $rate_id ); + + // Return HTML items + $order = wc_get_order( $order_id ); + include( 'admin/meta-boxes/views/html-order-items.php' ); + wp_die(); + } + + /** + * Reduce order item stock. */ public static function reduce_order_item_stock() { check_ajax_referer( 'order-item', 'security' ); - + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } $order_id = absint( $_POST['order_id'] ); $order_item_ids = isset( $_POST['order_item_ids'] ) ? $_POST['order_item_ids'] : array(); $order_item_qty = isset( $_POST['order_item_qty'] ) ? $_POST['order_item_qty'] : array(); - $order = get_order( $order_id ); + $order = wc_get_order( $order_id ); $order_items = $order->get_items(); $return = array(); - if ( $order && ! empty( $order_items ) && sizeof( $order_item_ids ) > 0 ) { - foreach ( $order_items as $item_id => $order_item ) { - // Only reduce checked items if ( ! in_array( $item_id, $order_item_ids ) ) { continue; } - - $_product = $order->get_product_from_item( $order_item ); - - if ( $_product->exists() && $_product->managing_stock() && isset( $order_item_qty[ $item_id ] ) && $order_item_qty[ $item_id ] > 0 ) { + $_product = $order_item->get_product(); + if ( $_product && $_product->exists() && $_product->managing_stock() && isset( $order_item_qty[ $item_id ] ) && $order_item_qty[ $item_id ] > 0 ) { $stock_change = apply_filters( 'woocommerce_reduce_order_stock_quantity', $order_item_qty[ $item_id ], $item_id ); - $new_stock = $_product->reduce_stock( $stock_change ); - - $return[] = sprintf( __( 'Item #%s stock reduced from %s to %s.', 'woocommerce' ), $order_item['product_id'], $new_stock + $stock_change, $new_stock ); - $order->add_order_note( sprintf( __( 'Item #%s stock reduced from %s to %s.', 'woocommerce' ), $order_item['product_id'], $new_stock + $stock_change, $new_stock ) ); - $order->send_stock_notifications( $_product, $new_stock, $order_item_qty[ $item_id ] ); + $new_stock = wc_update_product_stock( $_product, $stock_change, 'decrease' ); + $item_name = $_product->get_sku() ? $_product->get_sku() : $_product->get_id(); + $note = sprintf( __( 'Item %1$s stock reduced from %2$s to %3$s.', 'woocommerce' ), $item_name, $new_stock + $stock_change, $new_stock ); + $return[] = $note; + $order->add_order_note( $note ); } } - do_action( 'woocommerce_reduce_order_stock', $order ); - if ( empty( $return ) ) { $return[] = __( 'No products had their stock reduced - they may not have stock management enabled.', 'woocommerce' ); } - - echo implode( ', ', $return ); + echo wp_kses_post( implode( ', ', $return ) ); } - - die(); + wp_die(); } /** - * Increase order item stock + * Increase order item stock. */ public static function increase_order_item_stock() { check_ajax_referer( 'order-item', 'security' ); - + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } $order_id = absint( $_POST['order_id'] ); $order_item_ids = isset( $_POST['order_item_ids'] ) ? $_POST['order_item_ids'] : array(); $order_item_qty = isset( $_POST['order_item_qty'] ) ? $_POST['order_item_qty'] : array(); - $order = get_order( $order_id ); + $order = wc_get_order( $order_id ); $order_items = $order->get_items(); $return = array(); - if ( $order && ! empty( $order_items ) && sizeof( $order_item_ids ) > 0 ) { - foreach ( $order_items as $item_id => $order_item ) { - // Only reduce checked items if ( ! in_array( $item_id, $order_item_ids ) ) { continue; } - - $_product = $order->get_product_from_item( $order_item ); - - if ( $_product->exists() && $_product->managing_stock() && isset( $order_item_qty[ $item_id ] ) && $order_item_qty[ $item_id ] > 0 ) { - - $old_stock = $_product->stock; + $_product = $order_item->get_product(); + if ( $_product && $_product->exists() && $_product->managing_stock() && isset( $order_item_qty[ $item_id ] ) && $order_item_qty[ $item_id ] > 0 ) { + $old_stock = $_product->get_stock_quantity(); $stock_change = apply_filters( 'woocommerce_restore_order_stock_quantity', $order_item_qty[ $item_id ], $item_id ); - $new_quantity = $_product->increase_stock( $stock_change ); - - $return[] = sprintf( __( 'Item #%s stock increased from %s to %s.', 'woocommerce' ), $order_item['product_id'], $old_stock, $new_quantity ); - $order->add_order_note( sprintf( __( 'Item #%s stock increased from %s to %s.', 'woocommerce' ), $order_item['product_id'], $old_stock, $new_quantity ) ); + $new_quantity = wc_update_product_stock( $_product, $stock_change, 'increase' ); + $item_name = $_product->get_sku() ? $_product->get_sku() : $_product->get_id(); + $note = sprintf( __( 'Item %1$s stock increased from %2$s to %3$s.', 'woocommerce' ), $item_name, $old_stock, $new_quantity ); + $return[] = $note; + $order->add_order_note( $note ); } } - do_action( 'woocommerce_restore_order_stock', $order ); - if ( empty( $return ) ) { $return[] = __( 'No products had their stock increased - they may not have stock management enabled.', 'woocommerce' ); } - - echo implode( ', ', $return ); + echo wp_kses_post( implode( ', ', $return ) ); } - - die(); + wp_die(); } /** - * Add some meta to a line item - */ - public static function add_order_item_meta() { - check_ajax_referer( 'order-item', 'security' ); - - $meta_id = wc_add_order_item_meta( absint( $_POST['order_item_id'] ), __( 'Name', 'woocommerce' ), __( 'Value', 'woocommerce' ) ); - - if ( $meta_id ) { - echo ''; - } - - die(); - } - - /** - * Remove meta from a line item - */ - public static function remove_order_item_meta() { - global $wpdb; - - check_ajax_referer( 'order-item', 'security' ); - - $meta_id = absint( $_POST['meta_id'] ); - - $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_id = %d", $meta_id ) ); - - die(); - } - - /** - * Calc line tax + * Calc line tax. */ public static function calc_line_taxes() { - global $wpdb; - check_ajax_referer( 'calc-totals', 'security' ); - $tax = new WC_Tax(); - - $taxes = $tax_rows = $item_taxes = $shipping_taxes = array(); - $order_id = absint( $_POST['order_id'] ); - $order = get_order( $order_id ); - $country = strtoupper( esc_attr( $_POST['country'] ) ); - $state = strtoupper( esc_attr( $_POST['state'] ) ); - $postcode = strtoupper( esc_attr( $_POST['postcode'] ) ); - $city = sanitize_title( esc_attr( $_POST['city'] ) ); - $items = isset( $_POST['items'] ) ? $_POST['items'] : array(); - $shipping = $_POST['shipping']; - $item_tax = 0; - - // Calculate sales tax first - if ( sizeof( $items ) > 0 ) { - foreach( $items as $item_id => $item ) { - $item_id = absint( $item_id ); - $line_subtotal = isset( $item['line_subtotal'] ) ? wc_format_decimal( $item['line_subtotal'] ) : 0; - $line_total = wc_format_decimal( $item['line_total'] ); - $tax_class = sanitize_text_field( $item['tax_class'] ); - $product_id = $order->get_item_meta( $item_id, '_product_id', true ); - - // Get product details - if ( get_post_type( $product_id ) == 'product' ) { - $_product = get_product( $product_id ); - $item_tax_status = $_product->get_tax_status(); - } else { - $item_tax_status = 'taxable'; - } - - if ( '0' !== $tax_class && 'taxable' === $item_tax_status ) { - $tax_rates = WC_Tax::find_rates( array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - 'tax_class' => $tax_class - ) ); - - $line_subtotal_taxes = WC_Tax::calc_tax( $line_subtotal, $tax_rates, false ); - $line_taxes = WC_Tax::calc_tax( $line_total, $tax_rates, false ); - $line_subtotal_tax = max( 0, array_sum( $line_subtotal_taxes ) ); - $line_tax = max( 0, array_sum( $line_taxes ) ); - - $item_taxes[ $item_id ] = array( - 'line_subtotal_tax' => wc_format_localized_price( $line_subtotal_tax ), - 'line_tax' => wc_format_localized_price( $line_tax ) - ); - - $item_tax += $line_tax; - - // Sum the item taxes - foreach ( array_keys( $taxes + $line_taxes ) as $key ) { - $taxes[ $key ] = ( isset( $line_taxes[ $key ] ) ? $line_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 ); - } - } - } + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); } - // Now calculate shipping tax - $matched_tax_rates = array(); + $order_id = absint( $_POST['order_id'] ); + $calculate_tax_args = array( + 'country' => strtoupper( wc_clean( $_POST['country'] ) ), + 'state' => strtoupper( wc_clean( $_POST['state'] ) ), + 'postcode' => strtoupper( wc_clean( $_POST['postcode'] ) ), + 'city' => strtoupper( wc_clean( $_POST['city'] ) ), + ); - $tax_rates = WC_Tax::find_rates( array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - 'tax_class' => '' - ) ); + // Parse the jQuery serialized items + $items = array(); + parse_str( $_POST['items'], $items ); - if ( $tax_rates ) { - foreach ( $tax_rates as $key => $rate ) { - if ( isset( $rate['shipping'] ) && 'yes' == $rate['shipping'] ) { - $matched_tax_rates[ $key ] = $rate; - } - } - } - - $shipping_taxes = WC_Tax::calc_shipping_tax( $shipping, $matched_tax_rates ); - $shipping_tax = WC_Tax::round( array_sum( $shipping_taxes ) ); - - // Remove old tax rows - $order->remove_order_items( 'tax' ); - - // Add tax rows - foreach ( array_keys( $taxes + $shipping_taxes ) as $tax_rate_id ) { - $order->add_tax( $tax_rate_id, isset( $taxes[ $tax_rate_id ] ) ? $taxes[ $tax_rate_id ] : 0, isset( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] : 0 ); - } - - ob_start(); - - foreach ( $order->get_taxes() as $item_id => $item ) { - include( 'admin/meta-boxes/views/html-order-tax.php' ); - } - - $tax_row_html = ob_get_clean(); - - wp_send_json( array( - 'item_tax' => $item_tax, - 'item_taxes' => $item_taxes, - 'shipping_tax' => $shipping_tax, - 'tax_row_html' => $tax_row_html - ) ); + // Save order items first + wc_save_order_items( $order_id, $items ); + // Grab the order and recalc taxes + $order = wc_get_order( $order_id ); + $order->calculate_taxes( $calculate_tax_args ); + $order->calculate_totals( false ); + include( 'admin/meta-boxes/views/html-order-items.php' ); + wp_die(); } /** - * Add order note via ajax + * Save order items via ajax. + */ + public static function save_order_items() { + check_ajax_referer( 'order-item', 'security' ); + + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + if ( isset( $_POST['order_id'], $_POST['items'] ) ) { + $order_id = absint( $_POST['order_id'] ); + + // Parse the jQuery serialized items + $items = array(); + parse_str( $_POST['items'], $items ); + + // Save order items + wc_save_order_items( $order_id, $items ); + + // Return HTML items + $order = wc_get_order( $order_id ); + include( 'admin/meta-boxes/views/html-order-items.php' ); + } + wp_die(); + } + + /** + * Load order items via ajax. + */ + public static function load_order_items() { + check_ajax_referer( 'order-item', 'security' ); + + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + // Return HTML items + $order_id = absint( $_POST['order_id'] ); + $order = wc_get_order( $order_id ); + include( 'admin/meta-boxes/views/html-order-items.php' ); + wp_die(); + } + + /** + * Add order note via ajax. */ public static function add_order_note() { - check_ajax_referer( 'add-order-note', 'security' ); - $post_id = (int) $_POST['post_id']; + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + $post_id = absint( $_POST['post_id'] ); $note = wp_kses_post( trim( stripslashes( $_POST['note'] ) ) ); $note_type = $_POST['note_type']; - $is_customer_note = $note_type == 'customer' ? 1 : 0; + $is_customer_note = ( 'customer' === $note_type ) ? 1 : 0; if ( $post_id > 0 ) { - $order = get_order( $post_id ); - $comment_id = $order->add_order_note( $note, $is_customer_note ); + $order = wc_get_order( $post_id ); + $comment_id = $order->add_order_note( $note, $is_customer_note, true ); echo '
  • '; echo wpautop( wptexturize( $note ) ); - echo '

    '.__( 'Delete note', 'woocommerce' ).'

    '; + echo '
  • ' . __( 'Delete note', 'woocommerce' ) . '

    '; echo ''; } - - // Quit out - die(); + wp_die(); } /** - * Delete order note via ajax + * Delete order note via ajax. */ public static function delete_order_note() { - check_ajax_referer( 'delete-order-note', 'security' ); + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + $note_id = (int) $_POST['note_id']; if ( $note_id > 0 ) { wp_delete_comment( $note_id ); } - - // Quit out - die(); + wp_die(); } /** - * Search for products and echo json + * Search for products and echo json. * - * @param string $x (default: '') - * @param string $post_types (default: array('product')) + * @param string $term (default: '') + * @param bool $include_variations in search or not */ - public static function json_search_products( $x = '', $post_types = array('product') ) { - + public static function json_search_products( $term = '', $include_variations = false ) { check_ajax_referer( 'search-products', 'security' ); - $term = (string) wc_clean( stripslashes( $_GET['term'] ) ); + $term = wc_clean( empty( $term ) ? stripslashes( $_GET['term'] ) : $term ); if ( empty( $term ) ) { - die(); + wp_die(); } - if ( is_numeric( $term ) ) { - - $args = array( - 'post_type' => $post_types, - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'post__in' => array(0, $term), - 'fields' => 'ids' - ); - - $args2 = array( - 'post_type' => $post_types, - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'post_parent' => $term, - 'fields' => 'ids' - ); - - $args3 = array( - 'post_type' => $post_types, - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'meta_query' => array( - array( - 'key' => '_sku', - 'value' => $term, - 'compare' => 'LIKE' - ) - ), - 'fields' => 'ids' - ); - - $posts = array_unique( array_merge( get_posts( $args ), get_posts( $args2 ), get_posts( $args3 ) ) ); - - } else { - - $args = array( - 'post_type' => $post_types, - 'post_status' => 'publish', - 'posts_per_page' => -1, - 's' => $term, - 'fields' => 'ids' - ); - - $args2 = array( - 'post_type' => $post_types, - 'post_status' => 'publish', - 'posts_per_page' => -1, - 'meta_query' => array( - array( - 'key' => '_sku', - 'value' => $term, - 'compare' => 'LIKE' - ) - ), - 'fields' => 'ids' - ); - - $posts = array_unique( array_merge( get_posts( $args ), get_posts( $args2 ) ) ); + $data_store = WC_Data_Store::load( 'product' ); + $ids = $data_store->search_products( $term, '', (bool) $include_variations ); + if ( ! empty( $_GET['exclude'] ) ) { + $ids = array_diff( $ids, (array) $_GET['exclude'] ); } - $found_products = array(); - - if ( $posts ) { - foreach ( $posts as $post ) { - $product = get_product( $post ); - - $found_products[ $post ] = $product->get_formatted_name(); - } + if ( ! empty( $_GET['include'] ) ) { + $ids = array_intersect( $ids, (array) $_GET['include'] ); } - $found_products = apply_filters( 'woocommerce_json_search_found_products', $found_products ); + if ( ! empty( $_GET['limit'] ) ) { + $ids = array_slice( $ids, 0, absint( $_GET['limit'] ) ); + } - wp_send_json( $found_products ); + $product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_editable' ); + $products = array(); + foreach ( $product_objects as $product_object ) { + $products[ $product_object->get_id() ] = rawurldecode( $product_object->get_formatted_name() ); + } + + wp_send_json( apply_filters( 'woocommerce_json_search_found_products', $products ) ); } /** - * Search for product variations and return json + * Search for product variations and return json. * - * @access public - * @return void * @see WC_AJAX::json_search_products() */ public static function json_search_products_and_variations() { - self::json_search_products( '', array('product', 'product_variation') ); + self::json_search_products( '', true ); } /** - * Search for customers and return json - */ - public static function json_search_customers() { - - check_ajax_referer( 'search-customers', 'security' ); - - $term = wc_clean( stripslashes( $_GET['term'] ) ); - - if ( empty( $term ) ) { - die(); - } - - $default = isset( $_GET['default'] ) ? $_GET['default'] : __( 'Guest', 'woocommerce' ); - - $found_customers = array( '' => $default ); - - add_action( 'pre_user_query', array( __CLASS__, 'json_search_customer_name' ) ); - - $customers_query = new WP_User_Query( apply_filters( 'woocommerce_json_search_customers_query', array( - 'fields' => 'all', - 'orderby' => 'display_name', - 'search' => '*' . $term . '*', - 'search_columns' => array( 'ID', 'user_login', 'user_email', 'user_nicename' ) - ) ) ); - - remove_action( 'pre_user_query', array( __CLASS__, 'json_search_customer_name' ) ); - - $customers = $customers_query->get_results(); - - if ( $customers ) { - foreach ( $customers as $customer ) { - $found_customers[ $customer->ID ] = $customer->display_name . ' (#' . $customer->ID . ' – ' . sanitize_email( $customer->user_email ) . ')'; - } - } - - wp_send_json( $found_customers ); - - } - - /** - * Search for downloadable product variations and return json + * Search for downloadable product variations and return json. * - * @access public - * @return void * @see WC_AJAX::json_search_products() */ public static function json_search_downloadable_products_and_variations() { - $term = (string) wc_clean( stripslashes( $_GET['term'] ) ); + check_ajax_referer( 'search-products', 'security' ); - $args = array( - 'post_type' => array( 'product', 'product_variation' ), - 'posts_per_page' => -1, - 'post_status' => 'publish', - 'order' => 'ASC', - 'orderby' => 'parent title', - 'meta_query' => array( - array( - 'key' => '_downloadable', - 'value' => 'yes' - ) - ), - 's' => $term - ); + $term = (string) wc_clean( stripslashes( $_GET['term'] ) ); + $data_store = WC_Data_Store::load( 'product' ); + $ids = $data_store->search_products( $term, 'downloadable', true ); - $posts = get_posts( $args ); - $found_products = array(); - - if ( $posts ) { - foreach ( $posts as $post ) { - $product = get_product( $post->ID ); - $found_products[ $post->ID ] = $product->get_formatted_name(); - } + if ( ! empty( $_GET['exclude'] ) ) { + $ids = array_diff( $ids, (array) $_GET['exclude'] ); } - wp_send_json( $found_products ); + if ( ! empty( $_GET['include'] ) ) { + $ids = array_intersect( $ids, (array) $_GET['include'] ); + } + if ( ! empty( $_GET['limit'] ) ) { + $ids = array_slice( $ids, 0, absint( $_GET['limit'] ) ); + } + + $product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_editable' ); + $products = array(); + + foreach ( $product_objects as $product_object ) { + $products[ $product_object->get_id() ] = rawurldecode( $product_object->get_formatted_name() ); + } + + wp_send_json( $products ); } /** - * When searching using the WP_User_Query, search names (user meta) too - * @param object $query - * @return object + * Search for customers and return json. */ - public static function json_search_customer_name( $query ) { - global $wpdb; + public static function json_search_customers() { + ob_start(); - $term = wc_clean( stripslashes( $_GET['term'] ) ); + check_ajax_referer( 'search-customers', 'security' ); - $query->query_from .= " INNER JOIN {$wpdb->usermeta} AS user_name ON {$wpdb->users}.ID = user_name.user_id AND ( user_name.meta_key = 'first_name' OR user_name.meta_key = 'last_name' ) "; - $query->query_where .= $wpdb->prepare( " OR user_name.meta_value LIKE %s ", '%' . like_escape( $term ) . '%' ); + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + $term = wc_clean( stripslashes( $_GET['term'] ) ); + $exclude = array(); + $limit = ''; + + if ( empty( $term ) ) { + wp_die(); + } + + // Search by ID. + if ( is_numeric( $term ) ) { + $customer = new WC_Customer( intval( $term ) ); + + // Customer does not exists. + if ( 0 === $customer->get_id() ) { + wp_die(); + } + + $ids = array( $customer->get_id() ); + } else { + $data_store = WC_Data_Store::load( 'customer' ); + + // If search is smaller than 3 characters, limit result set to avoid + // too many rows being returned. + if ( 3 > strlen( $term ) ) { + $limit = 20; + } + $ids = $data_store->search_customers( $term, $limit ); + } + + $found_customers = array(); + + if ( ! empty( $_GET['exclude'] ) ) { + $ids = array_diff( $ids, (array) $_GET['exclude'] ); + } + + foreach ( $ids as $id ) { + $customer = new WC_Customer( $id ); + /* translators: 1: user display name 2: user ID 3: user email */ + $found_customers[ $id ] = sprintf( + esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), + $customer->get_first_name() . ' ' . $customer->get_last_name(), + $customer->get_id(), + $customer->get_email() + ); + } + + wp_send_json( apply_filters( 'woocommerce_json_search_found_customers', $found_customers ) ); } /** - * Ajax request handling for categories ordering + * Ajax request handling for categories ordering. */ public static function term_ordering() { + + // check permissions again and make sure we have what we need + if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) { + wp_die( -1 ); + } + $id = (int) $_POST['id']; $next_id = isset( $_POST['nextid'] ) && (int) $_POST['nextid'] ? (int) $_POST['nextid'] : null; $taxonomy = isset( $_POST['thetaxonomy'] ) ? esc_attr( $_POST['thetaxonomy'] ) : null; - $term = get_term_by('id', $id, $taxonomy); + $term = get_term_by( 'id', $id, $taxonomy ); if ( ! $id || ! $term || ! $taxonomy ) { - die(0); + wp_die( 0 ); } wc_reorder_terms( $term, $next_id, $taxonomy ); @@ -1536,95 +1294,1086 @@ class WC_AJAX { if ( $term && sizeof( $children ) ) { echo 'children'; - die(); + wp_die(); } } /** - * Ajax request handling for product ordering + * Ajax request handling for product ordering. * - * Based on Simple Page Ordering by 10up (http://wordpress.org/extend/plugins/simple-page-ordering/) + * Based on Simple Page Ordering by 10up (https://wordpress.org/plugins/simple-page-ordering/). */ public static function product_ordering() { global $wpdb; - // check permissions again and make sure we have what we need - if ( ! current_user_can('edit_products') || empty( $_POST['id'] ) || ( ! isset( $_POST['previd'] ) && ! isset( $_POST['nextid'] ) ) ) { - die(-1); + if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) { + wp_die( -1 ); } - // real post? - if ( ! $post = get_post( $_POST['id'] ) ) { - die(-1); + $sorting_id = absint( $_POST['id'] ); + $previd = absint( isset( $_POST['previd'] ) ? $_POST['previd'] : 0 ); + $nextid = absint( isset( $_POST['nextid'] ) ? $_POST['nextid'] : 0 ); + $menu_orders = wp_list_pluck( $wpdb->get_results( "SELECT ID, menu_order FROM {$wpdb->posts} WHERE post_type = 'product' ORDER BY menu_order ASC, post_title ASC" ), 'menu_order', 'ID' ); + $index = 0; + + foreach ( $menu_orders as $id => $menu_order ) { + $id = absint( $id ); + + if ( $sorting_id === $id ) { + continue; + } + if ( $nextid === $id ) { + $index ++; + } + $index ++; + $menu_orders[ $id ] = $index; + $wpdb->update( $wpdb->posts, array( 'menu_order' => $index ), array( 'ID' => $id ) ); + + /** + * When a single product has gotten it's ordering updated. + * $id The product ID + * $index The new menu order + */ + do_action( 'woocommerce_after_single_product_ordering', $id, $index ); } - $previd = isset( $_POST['previd'] ) ? $_POST['previd'] : false; - $nextid = isset( $_POST['nextid'] ) ? $_POST['nextid'] : false; - $new_pos = array(); // store new positions for ajax + if ( isset( $menu_orders[ $previd ] ) ) { + $menu_orders[ $sorting_id ] = $menu_orders[ $previd ] + 1; + } elseif ( isset( $menu_orders[ $nextid ] ) ) { + $menu_orders[ $sorting_id ] = $menu_orders[ $nextid ] - 1; + } else { + $menu_orders[ $sorting_id ] = 0; + } - $siblings = $wpdb->get_results( $wpdb->prepare(' - SELECT ID, menu_order FROM %1$s AS posts - WHERE posts.post_type = \'product\' - AND posts.post_status IN ( \'publish\', \'pending\', \'draft\', \'future\', \'private\' ) - AND posts.ID NOT IN (%2$d) - ORDER BY posts.menu_order ASC, posts.ID DESC - ', $wpdb->posts, $post->ID) ); + $wpdb->update( $wpdb->posts, array( 'menu_order' => $menu_orders[ $sorting_id ] ), array( 'ID' => $sorting_id ) ); - $menu_order = 0; + do_action( 'woocommerce_after_product_ordering' ); + wp_send_json( $menu_orders ); + } - foreach ( $siblings as $sibling ) { + /** + * Handle a refund via the edit order screen. + */ + public static function refund_line_items() { + ob_start(); + + check_ajax_referer( 'order-item', 'security' ); + + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + $order_id = absint( $_POST['order_id'] ); + $refund_amount = wc_format_decimal( sanitize_text_field( $_POST['refund_amount'] ), wc_get_price_decimals() ); + $refund_reason = sanitize_text_field( $_POST['refund_reason'] ); + $line_item_qtys = json_decode( sanitize_text_field( stripslashes( $_POST['line_item_qtys'] ) ), true ); + $line_item_totals = json_decode( sanitize_text_field( stripslashes( $_POST['line_item_totals'] ) ), true ); + $line_item_tax_totals = json_decode( sanitize_text_field( stripslashes( $_POST['line_item_tax_totals'] ) ), true ); + $api_refund = 'true' === $_POST['api_refund']; + $restock_refunded_items = 'true' === $_POST['restock_refunded_items']; + $refund = false; + $response_data = array(); + + try { + $order = wc_get_order( $order_id ); + $order_items = $order->get_items(); + $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded(), wc_get_price_decimals() ); + + if ( ! $refund_amount || $max_refund < $refund_amount || 0 > $refund_amount ) { + throw new exception( __( 'Invalid refund amount', 'woocommerce' ) ); + } + + // Prepare line items which we are refunding + $line_items = array(); + $item_ids = array_unique( array_merge( array_keys( $line_item_qtys, $line_item_totals ) ) ); + + foreach ( $item_ids as $item_id ) { + $line_items[ $item_id ] = array( 'qty' => 0, 'refund_total' => 0, 'refund_tax' => array() ); + } + foreach ( $line_item_qtys as $item_id => $qty ) { + $line_items[ $item_id ]['qty'] = max( $qty, 0 ); + } + foreach ( $line_item_totals as $item_id => $total ) { + $line_items[ $item_id ]['refund_total'] = wc_format_decimal( $total ); + } + foreach ( $line_item_tax_totals as $item_id => $tax_totals ) { + $line_items[ $item_id ]['refund_tax'] = array_filter( array_map( 'wc_format_decimal', $tax_totals ) ); + } + + // Create the refund object. + $refund = wc_create_refund( array( + 'amount' => $refund_amount, + 'reason' => $refund_reason, + 'order_id' => $order_id, + 'line_items' => $line_items, + 'refund_payment' => $api_refund, + 'restock_items' => $restock_refunded_items, + ) ); + + if ( is_wp_error( $refund ) ) { + throw new Exception( $refund->get_error_message() ); + } + + if ( did_action( 'woocommerce_order_fully_refunded' ) ) { + $response_data['status'] = 'fully_refunded'; + } + + wp_send_json_success( $response_data ); + + } catch ( Exception $e ) { + if ( $refund && is_a( $refund, 'WC_Order_Refund' ) ) { + wp_delete_post( $refund->get_id(), true ); + } + wp_send_json_error( array( 'error' => $e->getMessage() ) ); + } + } + + /** + * Delete a refund. + */ + public static function delete_refund() { + check_ajax_referer( 'order-item', 'security' ); + + if ( ! current_user_can( 'edit_shop_orders' ) ) { + wp_die( -1 ); + } + + $refund_ids = array_map( 'absint', is_array( $_POST['refund_id'] ) ? $_POST['refund_id'] : array( $_POST['refund_id'] ) ); + foreach ( $refund_ids as $refund_id ) { + if ( $refund_id && 'shop_order_refund' === get_post_type( $refund_id ) ) { + $refund = wc_get_order( $refund_id ); + $order_id = $refund->get_parent_id(); + $refund->delete( true ); + do_action( 'woocommerce_refund_deleted', $refund_id, $order_id ); + } + } + wp_die(); + } + + /** + * Triggered when clicking the rating footer. + */ + public static function rated() { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( -1 ); + } + update_option( 'woocommerce_admin_footer_text_rated', 1 ); + wp_die(); + } + + /** + * Create/Update API key. + */ + public static function update_api_key() { + ob_start(); + + global $wpdb; + + check_ajax_referer( 'update-api-key', 'security' ); + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( -1 ); + } + + try { + if ( empty( $_POST['description'] ) ) { + throw new Exception( __( 'Description is missing.', 'woocommerce' ) ); + } + if ( empty( $_POST['user'] ) ) { + throw new Exception( __( 'User is missing.', 'woocommerce' ) ); + } + if ( empty( $_POST['permissions'] ) ) { + throw new Exception( __( 'Permissions is missing.', 'woocommerce' ) ); + } + + $key_id = absint( $_POST['key_id'] ); + $description = sanitize_text_field( wp_unslash( $_POST['description'] ) ); + $permissions = ( in_array( $_POST['permissions'], array( 'read', 'write', 'read_write' ) ) ) ? sanitize_text_field( $_POST['permissions'] ) : 'read'; + $user_id = absint( $_POST['user'] ); + + if ( 0 < $key_id ) { + $data = array( + 'user_id' => $user_id, + 'description' => $description, + 'permissions' => $permissions, + ); - // if this is the post that comes after our repositioned post, set our repositioned post position and increment menu order - if ( $nextid == $sibling->ID ) { $wpdb->update( - $wpdb->posts, + $wpdb->prefix . 'woocommerce_api_keys', + $data, + array( 'key_id' => $key_id ), array( - 'menu_order' => $menu_order + '%d', + '%s', + '%s', ), - array( 'ID' => $post->ID ), - array( '%d' ), array( '%d' ) ); - $new_pos[ $post->ID ] = $menu_order; - $menu_order++; - } - // if repositioned post has been set, and new items are already in the right order, we can stop - if ( isset( $new_pos[ $post->ID ] ) && $sibling->menu_order >= $menu_order ) { - break; - } + $data['consumer_key'] = ''; + $data['consumer_secret'] = ''; + $data['message'] = __( 'API Key updated successfully.', 'woocommerce' ); + } else { + $consumer_key = 'ck_' . wc_rand_hash(); + $consumer_secret = 'cs_' . wc_rand_hash(); - // set the menu order of the current sibling and increment the menu order - $wpdb->update( - $wpdb->posts, - array( - 'menu_order' => $menu_order - ), - array( 'ID' => $sibling->ID ), - array( '%d' ), - array( '%d' ) - ); - $new_pos[ $sibling->ID ] = $menu_order; - $menu_order++; - - if ( ! $nextid && $previd == $sibling->ID ) { - $wpdb->update( - $wpdb->posts, - array( - 'menu_order' => $menu_order - ), - array( 'ID' => $post->ID ), - array( '%d' ), - array( '%d' ) + $data = array( + 'user_id' => $user_id, + 'description' => $description, + 'permissions' => $permissions, + 'consumer_key' => wc_api_hash( $consumer_key ), + 'consumer_secret' => $consumer_secret, + 'truncated_key' => substr( $consumer_key, -7 ), ); - $new_pos[$post->ID] = $menu_order; - $menu_order++; + + $wpdb->insert( + $wpdb->prefix . 'woocommerce_api_keys', + $data, + array( + '%d', + '%s', + '%s', + '%s', + '%s', + '%s', + ) + ); + + $key_id = $wpdb->insert_id; + $data['consumer_key'] = $consumer_key; + $data['consumer_secret'] = $consumer_secret; + $data['message'] = __( 'API Key generated successfully. Make sure to copy your new keys now as the secret key will be hidden once you leave this page.', 'woocommerce' ); + $data['revoke_url'] = '' . __( 'Revoke key', 'woocommerce' ) . ''; } + wp_send_json_success( $data ); + } catch ( Exception $e ) { + wp_send_json_error( array( 'message' => $e->getMessage() ) ); + } + } + + /** + * Load variations via AJAX. + */ + public static function load_variations() { + ob_start(); + + check_ajax_referer( 'load-variations', 'security' ); + + if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) ) { + wp_die( -1 ); } - wp_send_json( $new_pos ); + // Set $post global so its available, like within the admin screens + global $post; + $loop = 0; + $product_id = absint( $_POST['product_id'] ); + $post = get_post( $product_id ); + $product_object = wc_get_product( $product_id ); + $per_page = ! empty( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10; + $page = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1; + $variations = wc_get_products( array( + 'status' => array( 'private', 'publish' ), + 'type' => 'variation', + 'parent' => $product_id, + 'limit' => $per_page, + 'page' => $page, + 'orderby' => array( + 'menu_order' => 'ASC', + 'ID' => 'DESC', + ), + 'return' => 'objects', + ) ); + + if ( $variations ) { + foreach ( $variations as $variation_object ) { + $variation_id = $variation_object->get_id(); + $variation = get_post( $variation_id ); + $variation_data = array_merge( array_map( 'maybe_unserialize', get_post_custom( $variation_id ) ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compat. + include( 'admin/meta-boxes/views/html-variation-admin.php' ); + $loop++; + } + } + wp_die(); + } + + /** + * Save variations via AJAX. + */ + public static function save_variations() { + ob_start(); + + check_ajax_referer( 'save-variations', 'security' ); + + // Check permissions again and make sure we have what we need + if ( ! current_user_can( 'edit_products' ) || empty( $_POST ) || empty( $_POST['product_id'] ) ) { + wp_die( -1 ); + } + + $product_id = absint( $_POST['product_id'] ); + WC_Admin_Meta_Boxes::$meta_box_errors = array(); + WC_Meta_Box_Product_Data::save_variations( $product_id, get_post( $product_id ) ); + + do_action( 'woocommerce_ajax_save_product_variations', $product_id ); + + if ( $errors = WC_Admin_Meta_Boxes::$meta_box_errors ) { + echo '
    '; + + foreach ( $errors as $error ) { + echo '

    ' . wp_kses_post( $error ) . '

    '; + } + + echo ''; + echo '
    '; + + delete_option( 'woocommerce_meta_box_errors' ); + } + + wp_die(); + } + + /** + * Bulk action - Toggle Enabled. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_toggle_enabled( $variations, $data ) { + foreach ( $variations as $variation_id ) { + $variation = wc_get_product( $variation_id ); + $variation->set_status( 'private' === $variation->get_status( 'edit' ) ? 'publish' : 'private' ); + $variation->save(); + } + } + + /** + * Bulk action - Toggle Downloadable Checkbox. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_toggle_downloadable( $variations, $data ) { + self::variation_bulk_toggle( $variations, 'downloadable' ); + } + + /** + * Bulk action - Toggle Virtual Checkbox. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_toggle_virtual( $variations, $data ) { + self::variation_bulk_toggle( $variations, 'virtual' ); + } + + /** + * Bulk action - Toggle Manage Stock Checkbox. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_toggle_manage_stock( $variations, $data ) { + self::variation_bulk_toggle( $variations, 'manage_stock' ); + } + + /** + * Bulk action - Set Regular Prices. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_regular_price( $variations, $data ) { + self::variation_bulk_set( $variations, 'regular_price', $data['value'] ); + } + + /** + * Bulk action - Set Sale Prices. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_sale_price( $variations, $data ) { + self::variation_bulk_set( $variations, 'sale_price', $data['value'] ); + } + + /** + * Bulk action - Set Stock Status as In Stock. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_stock_status_instock( $variations, $data ) { + self::variation_bulk_set( $variations, 'stock_status', 'instock' ); + } + + /** + * Bulk action - Set Stock Status as Out of Stock. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_stock_status_outofstock( $variations, $data ) { + self::variation_bulk_set( $variations, 'stock_status', 'outofstock' ); + } + + /** + * Bulk action - Set Stock. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_stock( $variations, $data ) { + if ( ! isset( $data['value'] ) ) { + return; + } + + $quantity = wc_stock_amount( wc_clean( $data['value'] ) ); + + foreach ( $variations as $variation_id ) { + $variation = wc_get_product( $variation_id ); + if ( $variation->managing_stock() ) { + $variation->set_stock_quantity( $quantity ); + } else { + $variation->set_stock_quantity( null ); + } + $variation->save(); + } + } + + /** + * Bulk action - Set Weight. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_weight( $variations, $data ) { + self::variation_bulk_set( $variations, 'weight', $data['value'] ); + } + + /** + * Bulk action - Set Length. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_length( $variations, $data ) { + self::variation_bulk_set( $variations, 'length', $data['value'] ); + } + + /** + * Bulk action - Set Width. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_width( $variations, $data ) { + self::variation_bulk_set( $variations, 'width', $data['value'] ); + } + + /** + * Bulk action - Set Height. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_height( $variations, $data ) { + self::variation_bulk_set( $variations, 'height', $data['value'] ); + } + + /** + * Bulk action - Set Download Limit. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_download_limit( $variations, $data ) { + self::variation_bulk_set( $variations, 'download_limit', $data['value'] ); + } + + /** + * Bulk action - Set Download Expiry. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_download_expiry( $variations, $data ) { + self::variation_bulk_set( $variations, 'download_expiry', $data['value'] ); + } + + /** + * Bulk action - Delete all. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_delete_all( $variations, $data ) { + if ( isset( $data['allowed'] ) && 'true' === $data['allowed'] ) { + foreach ( $variations as $variation_id ) { + $variation = wc_get_product( $variation_id ); + $variation->delete( true ); + } + } + } + + /** + * Bulk action - Sale Schedule. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_sale_schedule( $variations, $data ) { + if ( ! isset( $data['date_from'] ) && ! isset( $data['date_to'] ) ) { + return; + } + + foreach ( $variations as $variation_id ) { + $variation = wc_get_product( $variation_id ); + + if ( 'false' !== $data['date_from'] ) { + $variation->set_date_on_sale_from( wc_clean( $data['date_from'] ) ); + } + + if ( 'false' !== $data['date_to'] ) { + $variation->set_date_on_sale_to( wc_clean( $data['date_to'] ) ); + } + + $variation->save(); + } + } + + /** + * Bulk action - Increase Regular Prices. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_regular_price_increase( $variations, $data ) { + self::variation_bulk_adjust_price( $variations, 'regular_price', '+', wc_clean( $data['value'] ) ); + } + + /** + * Bulk action - Decrease Regular Prices. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_regular_price_decrease( $variations, $data ) { + self::variation_bulk_adjust_price( $variations, 'regular_price', '-', wc_clean( $data['value'] ) ); + } + + /** + * Bulk action - Increase Sale Prices. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_sale_price_increase( $variations, $data ) { + self::variation_bulk_adjust_price( $variations, 'sale_price', '+', wc_clean( $data['value'] ) ); + } + + /** + * Bulk action - Decrease Sale Prices. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param array $data + */ + private static function variation_bulk_action_variable_sale_price_decrease( $variations, $data ) { + self::variation_bulk_adjust_price( $variations, 'sale_price', '-', wc_clean( $data['value'] ) ); + } + + /** + * Bulk action - Set Price. + * @access private + * @used-by bulk_edit_variations + * @param array $variations + * @param string $operator + or - + * @param string $field price being adjusted _regular_price or _sale_price + * @param string $value Price or Percent + */ + private static function variation_bulk_adjust_price( $variations, $field, $operator, $value ) { + foreach ( $variations as $variation_id ) { + $variation = wc_get_product( $variation_id ); + $field_value = $variation->{"get_$field"}( 'edit' ); + + if ( '%' === substr( $value, -1 ) ) { + $percent = wc_format_decimal( substr( $value, 0, -1 ) ); + $field_value += ( ( $field_value / 100 ) * $percent ) * "{$operator}1"; + } else { + $field_value += $value * "{$operator}1"; + } + + $variation->{"set_$field"}( $field_value ); + $variation->save(); + } + } + + /** + * Bulk set convenience function. + * @access private + * @param array $variations + * @param string $field + * @param string $value + */ + private static function variation_bulk_set( $variations, $field, $value ) { + foreach ( $variations as $variation_id ) { + $variation = wc_get_product( $variation_id ); + $variation->{ "set_$field" }( wc_clean( $value ) ); + $variation->save(); + } + } + + /** + * Bulk toggle convenience function. + * @access private + * @param array $variations + * @param string $field + */ + private static function variation_bulk_toggle( $variations, $field ) { + foreach ( $variations as $variation_id ) { + $variation = wc_get_product( $variation_id ); + $prev_value = $variation->{ "get_$field" }( 'edit' ); + $variation->{ "set_$field" }( ! $prev_value ); + $variation->save(); + } + } + + /** + * Bulk edit variations via AJAX. + * @uses WC_AJAX::variation_bulk_set() + * @uses WC_AJAX::variation_bulk_adjust_price() + * @uses WC_AJAX::variation_bulk_action_variable_sale_price_decrease() + * @uses WC_AJAX::variation_bulk_action_variable_sale_price_increase() + * @uses WC_AJAX::variation_bulk_action_variable_regular_price_decrease() + * @uses WC_AJAX::variation_bulk_action_variable_regular_price_increase() + * @uses WC_AJAX::variation_bulk_action_variable_sale_schedule() + * @uses WC_AJAX::variation_bulk_action_delete_all() + * @uses WC_AJAX::variation_bulk_action_variable_download_expiry() + * @uses WC_AJAX::variation_bulk_action_variable_download_limit() + * @uses WC_AJAX::variation_bulk_action_variable_height() + * @uses WC_AJAX::variation_bulk_action_variable_width() + * @uses WC_AJAX::variation_bulk_action_variable_length() + * @uses WC_AJAX::variation_bulk_action_variable_weight() + * @uses WC_AJAX::variation_bulk_action_variable_stock() + * @uses WC_AJAX::variation_bulk_action_variable_sale_price() + * @uses WC_AJAX::variation_bulk_action_variable_regular_price() + * @uses WC_AJAX::variation_bulk_action_toggle_manage_stock() + * @uses WC_AJAX::variation_bulk_action_toggle_virtual() + * @uses WC_AJAX::variation_bulk_action_toggle_downloadable() + * @uses WC_AJAX::variation_bulk_action_toggle_enabled + */ + public static function bulk_edit_variations() { + ob_start(); + + check_ajax_referer( 'bulk-edit-variations', 'security' ); + + // Check permissions again and make sure we have what we need + if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) || empty( $_POST['bulk_action'] ) ) { + wp_die( -1 ); + } + + $product_id = absint( $_POST['product_id'] ); + $bulk_action = wc_clean( $_POST['bulk_action'] ); + $data = ! empty( $_POST['data'] ) ? array_map( 'wc_clean', $_POST['data'] ) : array(); + $variations = array(); + + if ( apply_filters( 'woocommerce_bulk_edit_variations_need_children', true ) ) { + $variations = get_posts( array( + 'post_parent' => $product_id, + 'posts_per_page' => -1, + 'post_type' => 'product_variation', + 'fields' => 'ids', + 'post_status' => array( 'publish', 'private' ), + ) ); + } + + if ( method_exists( __CLASS__, "variation_bulk_action_$bulk_action" ) ) { + call_user_func( array( __CLASS__, "variation_bulk_action_$bulk_action" ), $variations, $data ); + } else { + do_action( 'woocommerce_bulk_edit_variations_default', $bulk_action, $data, $product_id, $variations ); + } + + do_action( 'woocommerce_bulk_edit_variations', $bulk_action, $data, $product_id, $variations ); + WC_Product_Variable::sync( $product_id ); + wc_delete_product_transients( $product_id ); + wp_die(); + } + + /** + * Handle submissions from assets/js/settings-views-html-settings-tax.js Backbone model. + */ + public static function tax_rates_save_changes() { + if ( ! isset( $_POST['wc_tax_nonce'], $_POST['changes'] ) ) { + wp_send_json_error( 'missing_fields' ); + exit; + } + + $current_class = ! empty( $_POST['current_class'] ) ? $_POST['current_class'] : ''; // This is sanitized seven lines later. + + if ( ! wp_verify_nonce( $_POST['wc_tax_nonce'], 'wc_tax_nonce-class:' . $current_class ) ) { + wp_send_json_error( 'bad_nonce' ); + exit; + } + + $current_class = WC_Tax::format_tax_rate_class( $current_class ); + + // Check User Caps + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'missing_capabilities' ); + exit; + } + + $changes = $_POST['changes']; + foreach ( $changes as $tax_rate_id => $data ) { + if ( isset( $data['deleted'] ) ) { + if ( isset( $data['newRow'] ) ) { + // So the user added and deleted a new row. + // That's fine, it's not in the database anyways. NEXT! + continue; + } + WC_Tax::_delete_tax_rate( $tax_rate_id ); + } + + $tax_rate = array_intersect_key( $data, array( + 'tax_rate_country' => 1, + 'tax_rate_state' => 1, + 'tax_rate' => 1, + 'tax_rate_name' => 1, + 'tax_rate_priority' => 1, + 'tax_rate_compound' => 1, + 'tax_rate_shipping' => 1, + 'tax_rate_order' => 1, + ) ); + + if ( isset( $data['newRow'] ) ) { + // Hurrah, shiny and new! + $tax_rate['tax_rate_class'] = $current_class; + $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate ); + } else { + // Updating an existing rate ... + if ( ! empty( $tax_rate ) ) { + WC_Tax::_update_tax_rate( $tax_rate_id, $tax_rate ); + } + } + + if ( isset( $data['postcode'] ) ) { + $postcode = array_map( 'wc_clean', $data['postcode'] ); + $postcode = array_map( 'wc_normalize_postcode', $postcode ); + WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, $postcode ); + } + if ( isset( $data['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $tax_rate_id, array_map( 'wc_clean', $data['city'] ) ); + } + } + + wp_send_json_success( array( + 'rates' => WC_Tax::get_rates_for_tax_class( $current_class ), + ) ); + } + + /** + * Handle submissions from assets/js/wc-shipping-zones.js Backbone model. + */ + public static function shipping_zones_save_changes() { + if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['changes'] ) ) { + wp_send_json_error( 'missing_fields' ); + exit; + } + + if ( ! wp_verify_nonce( $_POST['wc_shipping_zones_nonce'], 'wc_shipping_zones_nonce' ) ) { + wp_send_json_error( 'bad_nonce' ); + exit; + } + + // Check User Caps + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'missing_capabilities' ); + exit; + } + + $changes = $_POST['changes']; + foreach ( $changes as $zone_id => $data ) { + if ( isset( $data['deleted'] ) ) { + if ( isset( $data['newRow'] ) ) { + // So the user added and deleted a new row. + // That's fine, it's not in the database anyways. NEXT! + continue; + } + WC_Shipping_Zones::delete_zone( $zone_id ); + continue; + } + + $zone_data = array_intersect_key( $data, array( + 'zone_id' => 1, + 'zone_order' => 1, + ) ); + + if ( isset( $zone_data['zone_id'] ) ) { + $zone = new WC_Shipping_Zone( $zone_data['zone_id'] ); + + if ( isset( $zone_data['zone_order'] ) ) { + $zone->set_zone_order( $zone_data['zone_order'] ); + } + + $zone->save(); + } + } + + wp_send_json_success( array( + 'zones' => WC_Shipping_Zones::get_zones(), + ) ); + } + + /** + * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model. + */ + public static function shipping_zone_add_method() { + if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['method_id'] ) ) { + wp_send_json_error( 'missing_fields' ); + exit; + } + + if ( ! wp_verify_nonce( $_POST['wc_shipping_zones_nonce'], 'wc_shipping_zones_nonce' ) ) { + wp_send_json_error( 'bad_nonce' ); + exit; + } + + // Check User Caps + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'missing_capabilities' ); + exit; + } + + $zone_id = wc_clean( $_POST['zone_id'] ); + $zone = new WC_Shipping_Zone( $zone_id ); + $instance_id = $zone->add_shipping_method( wc_clean( $_POST['method_id'] ) ); + + wp_send_json_success( array( + 'instance_id' => $instance_id, + 'zone_id' => $zone->get_id(), + 'zone_name' => $zone->get_zone_name(), + 'methods' => $zone->get_shipping_methods(), + ) ); + } + + /** + * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model. + */ + public static function shipping_zone_methods_save_changes() { + if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['changes'] ) ) { + wp_send_json_error( 'missing_fields' ); + exit; + } + + if ( ! wp_verify_nonce( $_POST['wc_shipping_zones_nonce'], 'wc_shipping_zones_nonce' ) ) { + wp_send_json_error( 'bad_nonce' ); + exit; + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'missing_capabilities' ); + exit; + } + + global $wpdb; + + $zone_id = wc_clean( $_POST['zone_id'] ); + $zone = new WC_Shipping_Zone( $zone_id ); + $changes = $_POST['changes']; + + if ( isset( $changes['zone_name'] ) ) { + $zone->set_zone_name( wc_clean( $changes['zone_name'] ) ); + } + + if ( isset( $changes['zone_locations'] ) ) { + $zone->clear_locations( array( 'state', 'country', 'continent' ) ); + $locations = array_filter( array_map( 'wc_clean', (array) $changes['zone_locations'] ) ); + foreach ( $locations as $location ) { + // Each posted location will be in the format type:code + $location_parts = explode( ':', $location ); + switch ( $location_parts[0] ) { + case 'state' : + $zone->add_location( $location_parts[1] . ':' . $location_parts[2], 'state' ); + break; + case 'country' : + $zone->add_location( $location_parts[1], 'country' ); + break; + case 'continent' : + $zone->add_location( $location_parts[1], 'continent' ); + break; + } + } + } + + if ( isset( $changes['zone_postcodes'] ) ) { + $zone->clear_locations( 'postcode' ); + $postcodes = array_filter( array_map( 'strtoupper', array_map( 'wc_clean', explode( "\n", $changes['zone_postcodes'] ) ) ) ); + foreach ( $postcodes as $postcode ) { + $zone->add_location( $postcode, 'postcode' ); + } + } + + if ( isset( $changes['methods'] ) ) { + foreach ( $changes['methods'] as $instance_id => $data ) { + $method_id = $wpdb->get_var( $wpdb->prepare( "SELECT method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d", $instance_id ) ); + + if ( isset( $data['deleted'] ) ) { + $shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id ); + $option_key = $shipping_method->get_instance_option_key(); + if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) { + delete_option( $option_key ); + do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method_id, $zone_id ); + } + continue; + } + + $method_data = array_intersect_key( $data, array( + 'method_order' => 1, + 'enabled' => 1, + ) ); + + if ( isset( $method_data['method_order'] ) ) { + $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $method_data['method_order'] ) ), array( 'instance_id' => absint( $instance_id ) ) ); + } + + if ( isset( $method_data['enabled'] ) ) { + $is_enabled = absint( 'yes' === $method_data['enabled'] ); + if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $is_enabled ), array( 'instance_id' => absint( $instance_id ) ) ) ) { + do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method_id, $zone_id, $is_enabled ); + } + } + } + } + + $zone->save(); + + wp_send_json_success( array( + 'zone_id' => $zone->get_id(), + 'zone_name' => $zone->get_zone_name(), + 'methods' => $zone->get_shipping_methods(), + ) ); + } + + /** + * Save method settings + */ + public static function shipping_zone_methods_save_settings() { + if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['instance_id'], $_POST['data'] ) ) { + wp_send_json_error( 'missing_fields' ); + exit; + } + + if ( ! wp_verify_nonce( $_POST['wc_shipping_zones_nonce'], 'wc_shipping_zones_nonce' ) ) { + wp_send_json_error( 'bad_nonce' ); + exit; + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'missing_capabilities' ); + exit; + } + + $instance_id = absint( $_POST['instance_id'] ); + $zone = WC_Shipping_Zones::get_zone_by( 'instance_id', $instance_id ); + $shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id ); + $shipping_method->set_post_data( $_POST['data'] ); + $shipping_method->process_admin_options(); + + wp_send_json_success( array( + 'zone_id' => $zone->get_id(), + 'zone_name' => $zone->get_zone_name(), + 'methods' => $zone->get_shipping_methods(), + 'errors' => $shipping_method->get_errors(), + ) ); + } + + /** + * Handle submissions from assets/js/wc-shipping-classes.js Backbone model. + */ + public static function shipping_classes_save_changes() { + if ( ! isset( $_POST['wc_shipping_classes_nonce'], $_POST['changes'] ) ) { + wp_send_json_error( 'missing_fields' ); + exit; + } + + if ( ! wp_verify_nonce( $_POST['wc_shipping_classes_nonce'], 'wc_shipping_classes_nonce' ) ) { + wp_send_json_error( 'bad_nonce' ); + exit; + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'missing_capabilities' ); + exit; + } + + $changes = $_POST['changes']; + + foreach ( $changes as $term_id => $data ) { + $term_id = absint( $term_id ); + + if ( isset( $data['deleted'] ) ) { + if ( isset( $data['newRow'] ) ) { + // So the user added and deleted a new row. + // That's fine, it's not in the database anyways. NEXT! + continue; + } + wp_delete_term( $term_id, 'product_shipping_class' ); + continue; + } + + $update_args = array(); + + if ( isset( $data['name'] ) ) { + $update_args['name'] = wc_clean( $data['name'] ); + } + + if ( isset( $data['slug'] ) ) { + $update_args['slug'] = wc_clean( $data['slug'] ); + } + + if ( isset( $data['description'] ) ) { + $update_args['description'] = wc_clean( $data['description'] ); + } + + if ( isset( $data['newRow'] ) ) { + $update_args = array_filter( $update_args ); + if ( empty( $update_args['name'] ) ) { + continue; + } + $term_id = wp_insert_term( $update_args['name'], 'product_shipping_class', $update_args ); + } else { + wp_update_term( $term_id, 'product_shipping_class', $update_args ); + } + + do_action( 'woocommerce_shipping_classes_save_class', $term_id, $data ); + } + + $wc_shipping = WC_Shipping::instance(); + + wp_send_json_success( array( + 'shipping_classes' => $wc_shipping->get_shipping_classes(), + ) ); } } diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 684428209c1..6b3a5b133f7 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -2,183 +2,290 @@ /** * WooCommerce API * - * Handles WC-API endpoint requests + * Handles WC-API endpoint requests. * - * @author WooThemes - * @category API - * @package WooCommerce/API - * @since 2.0 + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; +} -class WC_API { - - /** This is the major version for the REST API and takes - * first-order position in endpoint URLs - */ - const VERSION = 1; - - /** @var WC_API_Server the REST API server */ - public $server; +class WC_API extends WC_Legacy_API { /** - * Setup class - * - * @access public + * Setup class. * @since 2.0 - * @return WC_API */ public function __construct() { + parent::__construct(); - // add query vars - add_filter( 'query_vars', array( $this, 'add_query_vars'), 0 ); + // Add query vars. + add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); - // register API endpoints - add_action( 'init', array( $this, 'add_endpoint'), 0 ); + // Register API endpoints. + add_action( 'init', array( $this, 'add_endpoint' ), 0 ); - // handle REST/legacy API request - add_action( 'parse_request', array( $this, 'handle_api_requests'), 0 ); + // Handle wc-api endpoint requests. + add_action( 'parse_request', array( $this, 'handle_api_requests' ), 0 ); + + // Ensure payment gateways are initialized in time for API requests. + add_action( 'woocommerce_api_request', array( 'WC_Payment_Gateways', 'instance' ), 0 ); + + // WP REST API. + $this->rest_api_init(); } /** - * add_query_vars function. + * Add new query vars. * - * @access public * @since 2.0 - * @param $vars - * @return array + * @param array $vars + * @return string[] */ public function add_query_vars( $vars ) { + $vars = parent::add_query_vars( $vars ); $vars[] = 'wc-api'; - $vars[] = 'wc-api-route'; return $vars; } /** - * add_endpoint function. - * - * @access public + * WC API for payment gateway IPNs, etc. * @since 2.0 - * @return void */ - public function add_endpoint() { - - // REST API - add_rewrite_rule( '^wc-api\/v' . self::VERSION . '/?$', 'index.php?wc-api-route=/', 'top' ); - add_rewrite_rule( '^wc-api\/v' . self::VERSION .'(.*)?', 'index.php?wc-api-route=$matches[1]', 'top' ); - - // legacy API for payment gateway IPNs + public static function add_endpoint() { + parent::add_endpoint(); add_rewrite_endpoint( 'wc-api', EP_ALL ); } - /** - * API request - Trigger any API requests + * API request - Trigger any API requests. * - * @access public - * @since 2.0 - * @return void + * @since 2.0 + * @version 2.4 */ public function handle_api_requests() { global $wp; - if ( ! empty( $_GET['wc-api'] ) ) + if ( ! empty( $_GET['wc-api'] ) ) { $wp->query_vars['wc-api'] = $_GET['wc-api']; - - if ( ! empty( $_GET['wc-api-route'] ) ) - $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; - - // REST API request - if ( ! empty( $wp->query_vars['wc-api-route'] ) ) { - - define( 'WC_API_REQUEST', true ); - - // load required files - $this->includes(); - - $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] ); - - // load API resource classes - $this->register_resources( $this->server ); - - // Fire off the request - $this->server->serve_request(); - - exit; } - // legacy API requests + // wc-api endpoint requests. if ( ! empty( $wp->query_vars['wc-api'] ) ) { - // Buffer, we won't want any output here + // Buffer, we won't want any output here. ob_start(); - // Get API trigger - $api = strtolower( esc_attr( $wp->query_vars['wc-api'] ) ); + // No cache headers. + nocache_headers(); - // Load class if exists - if ( class_exists( $api ) ) - $api_class = new $api(); + // Clean the API request. + $api_request = strtolower( wc_clean( $wp->query_vars['wc-api'] ) ); - // Trigger actions - do_action( 'woocommerce_api_' . $api ); + // Trigger generic action before request hook. + do_action( 'woocommerce_api_request', $api_request ); - // Done, clear buffer and exit + // Is there actually something hooked into this API request? If not trigger 400 - Bad request. + status_header( has_action( 'woocommerce_api_' . $api_request ) ? 200 : 400 ); + + // Trigger an action which plugins can hook into to fulfill the request. + do_action( 'woocommerce_api_' . $api_request ); + + // Done, clear buffer and exit. ob_end_clean(); - die('1'); + die( '-1' ); } } - /** - * Include required files for REST API request - * - * @since 2.1 + * Init WP REST API. + * @since 2.6.0 */ - private function includes() { + private function rest_api_init() { + // REST API was included starting WordPress 4.4. + if ( ! class_exists( 'WP_REST_Server' ) ) { + return; + } - // API server / response handlers - include_once( 'api/class-wc-api-server.php' ); - include_once( 'api/interface-wc-api-handler.php' ); - include_once( 'api/class-wc-api-json-handler.php' ); - include_once( 'api/class-wc-api-xml-handler.php' ); + $this->rest_api_includes(); - // authentication - include_once( 'api/class-wc-api-authentication.php' ); - $this->authentication = new WC_API_Authentication(); - - include_once( 'api/class-wc-api-resource.php' ); - include_once( 'api/class-wc-api-orders.php' ); - include_once( 'api/class-wc-api-products.php' ); - include_once( 'api/class-wc-api-coupons.php' ); - include_once( 'api/class-wc-api-customers.php' ); - include_once( 'api/class-wc-api-reports.php' ); - - // allow plugins to load other response handlers or resource classes - do_action( 'woocommerce_api_loaded' ); + // Init REST API routes. + add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 ); } /** - * Register available API resources + * Include REST API classes. * - * @since 2.1 - * @param object $server the REST server + * @since 2.6.0 */ - public function register_resources( $server ) { + private function rest_api_includes() { + // Exception handler. + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-exception.php' ); - $api_classes = apply_filters( 'woocommerce_api_classes', - array( - 'WC_API_Customers', - 'WC_API_Orders', - 'WC_API_Products', - 'WC_API_Coupons', - 'WC_API_Reports', - ) + // Authentication. + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-authentication.php' ); + + // WP-API classes and functions. + include_once( dirname( __FILE__ ) . '/vendor/wp-rest-functions.php' ); + if ( ! class_exists( 'WP_REST_Controller' ) ) { + include_once( dirname( __FILE__ ) . '/vendor/abstract-wp-rest-controller.php' ); + } + + // Abstract controllers. + include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-controller.php' ); + include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-posts-controller.php' ); + include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-crud-controller.php' ); + include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-terms-controller.php' ); + include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-shipping-zones-controller.php' ); + include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-settings-api.php' ); + + // REST API v1 controllers. + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-coupons-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-customer-downloads-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-customers-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-orders-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-order-notes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-order-refunds-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-attribute-terms-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-attributes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-categories-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-reviews-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-shipping-classes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-tags-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-products-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-report-sales-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-report-top-sellers-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-reports-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-tax-classes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-taxes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-webhook-deliveries.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-webhooks-controller.php' ); + + // Legacy v2 code. + include_once( dirname( __FILE__ ) . '/api/legacy/class-wc-rest-legacy-coupons-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/class-wc-rest-legacy-orders-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/class-wc-rest-legacy-products-controller.php' ); + + // REST API v2 controllers. + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-coupons-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-customer-downloads-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-customers-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-orders-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-order-notes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-order-refunds-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-product-attribute-terms-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-product-attributes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-product-categories-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-product-reviews-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-product-shipping-classes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-product-tags-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-products-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-product-variations-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-report-sales-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-report-top-sellers-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-reports-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-settings-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-setting-options-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-shipping-zones-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-shipping-zone-locations-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-shipping-zone-methods-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-tax-classes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-taxes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-webhook-deliveries.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-webhooks-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-system-status-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-system-status-tools-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-shipping-methods-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/class-wc-rest-payment-gateways-controller.php' ); + } + + /** + * Register REST API routes. + * @since 2.6.0 + */ + public function register_rest_routes() { + // Register settings to the REST API. + $this->register_wp_admin_settings(); + + $controllers = array( + // v1 controllers. + 'WC_REST_Coupons_V1_Controller', + 'WC_REST_Customer_Downloads_V1_Controller', + 'WC_REST_Customers_V1_Controller', + 'WC_REST_Order_Notes_V1_Controller', + 'WC_REST_Order_Refunds_V1_Controller', + 'WC_REST_Orders_V1_Controller', + 'WC_REST_Product_Attribute_Terms_V1_Controller', + 'WC_REST_Product_Attributes_V1_Controller', + 'WC_REST_Product_Categories_V1_Controller', + 'WC_REST_Product_Reviews_V1_Controller', + 'WC_REST_Product_Shipping_Classes_V1_Controller', + 'WC_REST_Product_Tags_V1_Controller', + 'WC_REST_Products_V1_Controller', + 'WC_REST_Report_Sales_V1_Controller', + 'WC_REST_Report_Top_Sellers_V1_Controller', + 'WC_REST_Reports_V1_Controller', + 'WC_REST_Tax_Classes_V1_Controller', + 'WC_REST_Taxes_V1_Controller', + 'WC_REST_Webhook_Deliveries_V1_Controller', + 'WC_REST_Webhooks_V1_Controller', + + // v2 controllers. + 'WC_REST_Coupons_Controller', + 'WC_REST_Customer_Downloads_Controller', + 'WC_REST_Customers_Controller', + 'WC_REST_Order_Notes_Controller', + 'WC_REST_Order_Refunds_Controller', + 'WC_REST_Orders_Controller', + 'WC_REST_Product_Attribute_Terms_Controller', + 'WC_REST_Product_Attributes_Controller', + 'WC_REST_Product_Categories_Controller', + 'WC_REST_Product_Reviews_Controller', + 'WC_REST_Product_Shipping_Classes_Controller', + 'WC_REST_Product_Tags_Controller', + 'WC_REST_Products_Controller', + 'WC_REST_Product_Variations_Controller', + 'WC_REST_Report_Sales_Controller', + 'WC_REST_Report_Top_Sellers_Controller', + 'WC_REST_Reports_Controller', + 'WC_REST_Settings_Controller', + 'WC_REST_Setting_Options_Controller', + 'WC_REST_Shipping_Zones_Controller', + 'WC_REST_Shipping_Zone_Locations_Controller', + 'WC_REST_Shipping_Zone_Methods_Controller', + 'WC_REST_Tax_Classes_Controller', + 'WC_REST_Taxes_Controller', + 'WC_REST_Webhook_Deliveries_Controller', + 'WC_REST_Webhooks_Controller', + 'WC_REST_System_Status_Controller', + 'WC_REST_System_Status_Tools_Controller', + 'WC_REST_Shipping_Methods_Controller', + 'WC_REST_Payment_Gateways_Controller', ); - foreach ( $api_classes as $api_class ) { - $this->$api_class = new $api_class( $server ); + foreach ( $controllers as $controller ) { + $this->$controller = new $controller(); + $this->$controller->register_routes(); + } + } + + /** + * Register WC settings from WP-API to the REST API. + * @since 3.0.0 + */ + public function register_wp_admin_settings() { + $pages = WC_Admin_Settings::get_settings_pages(); + foreach ( $pages as $page ) { + new WC_Register_WP_Admin_Settings( $page, 'page' ); + } + + $emails = WC_Emails::instance(); + foreach ( $emails->get_emails() as $email ) { + new WC_Register_WP_Admin_Settings( $email, 'email' ); } } diff --git a/includes/class-wc-auth.php b/includes/class-wc-auth.php new file mode 100644 index 00000000000..0c4c69c0cc4 --- /dev/null +++ b/includes/class-wc-auth.php @@ -0,0 +1,402 @@ + __( 'Read', 'woocommerce' ), + 'write' => __( 'Write', 'woocommerce' ), + 'read_write' => __( 'Read/Write', 'woocommerce' ), + ); + + return $permissions[ $scope ]; + } + + /** + * Return a list of permissions a scope allows. + * + * @since 2.4.0 + * + * @param string $scope + * + * @return array + */ + protected function get_permissions_in_scope( $scope ) { + $permissions = array(); + switch ( $scope ) { + case 'read' : + $permissions[] = __( 'View coupons', 'woocommerce' ); + $permissions[] = __( 'View customers', 'woocommerce' ); + $permissions[] = __( 'View orders and sales reports', 'woocommerce' ); + $permissions[] = __( 'View products', 'woocommerce' ); + break; + case 'write' : + $permissions[] = __( 'Create webhooks', 'woocommerce' ); + $permissions[] = __( 'Create coupons', 'woocommerce' ); + $permissions[] = __( 'Create customers', 'woocommerce' ); + $permissions[] = __( 'Create orders', 'woocommerce' ); + $permissions[] = __( 'Create products', 'woocommerce' ); + break; + case 'read_write' : + $permissions[] = __( 'Create webhooks', 'woocommerce' ); + $permissions[] = __( 'View and manage coupons', 'woocommerce' ); + $permissions[] = __( 'View and manage customers', 'woocommerce' ); + $permissions[] = __( 'View and manage orders and sales reports', 'woocommerce' ); + $permissions[] = __( 'View and manage products', 'woocommerce' ); + break; + } + return apply_filters( 'woocommerce_api_permissions_in_scope', $permissions, $scope ); + } + + /** + * Build auth urls. + * + * @since 2.4.0 + * + * @param array $data + * @param string $endpoint + * + * @return string + */ + protected function build_url( $data, $endpoint ) { + $url = wc_get_endpoint_url( 'wc-auth/v' . self::VERSION, $endpoint, home_url( '/' ) ); + + return add_query_arg( array( + 'app_name' => wc_clean( $data['app_name'] ), + 'user_id' => wc_clean( $data['user_id'] ), + 'return_url' => urlencode( $this->get_formatted_url( $data['return_url'] ) ), + 'callback_url' => urlencode( $this->get_formatted_url( $data['callback_url'] ) ), + 'scope' => wc_clean( $data['scope'] ), + ), $url ); + } + + /** + * Decode and format a URL. + * @param string $url + * @return string + */ + protected function get_formatted_url( $url ) { + $url = urldecode( $url ); + + if ( ! strstr( $url, '://' ) ) { + $url = 'https://' . $url; + } + + return $url; + } + + /** + * Make validation. + * + * @since 2.4.0 + */ + protected function make_validation() { + $params = array( + 'app_name', + 'user_id', + 'return_url', + 'callback_url', + 'scope', + ); + + foreach ( $params as $param ) { + if ( empty( $_REQUEST[ $param ] ) ) { + /* translators: %s: parameter */ + throw new Exception( sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param ) ); + } + } + + if ( ! in_array( $_REQUEST['scope'], array( 'read', 'write', 'read_write' ) ) ) { + /* translators: %s: scope */ + throw new Exception( sprintf( __( 'Invalid scope %s', 'woocommerce' ), wc_clean( $_REQUEST['scope'] ) ) ); + } + + foreach ( array( 'return_url', 'callback_url' ) as $param ) { + $param = $this->get_formatted_url( $_REQUEST[ $param ] ); + + if ( false === filter_var( $param, FILTER_VALIDATE_URL ) ) { + /* translators: %s: url */ + throw new Exception( sprintf( __( 'The %s is not a valid URL', 'woocommerce' ), $param ) ); + } + } + + $callback_url = $this->get_formatted_url( $_REQUEST['callback_url'] ); + + if ( 0 !== stripos( $callback_url, 'https://' ) ) { + throw new Exception( __( 'The callback_url need to be over SSL', 'woocommerce' ) ); + } + } + + /** + * Create keys. + * + * @since 2.4.0 + * + * @param string $app_name + * @param string $app_user_id + * @param string $scope + * + * @return array + */ + protected function create_keys( $app_name, $app_user_id, $scope ) { + global $wpdb; + + /* translators: 1: app name 2: scope 3: date 4: time */ + $description = sprintf( + __( '%1$s - API %2$s (created on %3$s at %4$s).', 'woocommerce' ), + wc_clean( $app_name ), + $this->get_i18n_scope( $scope ), + date_i18n( wc_date_format() ), + date_i18n( wc_time_format() ) + ); + $user = wp_get_current_user(); + + // Created API keys. + $permissions = ( in_array( $scope, array( 'read', 'write', 'read_write' ) ) ) ? sanitize_text_field( $scope ) : 'read'; + $consumer_key = 'ck_' . wc_rand_hash(); + $consumer_secret = 'cs_' . wc_rand_hash(); + + $wpdb->insert( + $wpdb->prefix . 'woocommerce_api_keys', + array( + 'user_id' => $user->ID, + 'description' => $description, + 'permissions' => $permissions, + 'consumer_key' => wc_api_hash( $consumer_key ), + 'consumer_secret' => $consumer_secret, + 'truncated_key' => substr( $consumer_key, -7 ), + ), + array( + '%d', + '%s', + '%s', + '%s', + '%s', + '%s', + ) + ); + + return array( + 'key_id' => $wpdb->insert_id, + 'user_id' => $app_user_id, + 'consumer_key' => $consumer_key, + 'consumer_secret' => $consumer_secret, + 'key_permissions' => $permissions, + ); + } + + /** + * Post consumer data. + * + * @since 2.4.0 + * + * @param array $consumer_data + * @param string $url + * + * @return bool + * @throws Exception + */ + protected function post_consumer_data( $consumer_data, $url ) { + $params = array( + 'body' => json_encode( $consumer_data ), + 'timeout' => 60, + 'headers' => array( + 'Content-Type' => 'application/json;charset=' . get_bloginfo( 'charset' ), + ), + ); + + $response = wp_safe_remote_post( esc_url_raw( $url ), $params ); + + if ( is_wp_error( $response ) ) { + throw new Exception( $response->get_error_message() ); + } elseif ( 200 != $response['response']['code'] ) { + throw new Exception( __( 'An error occurred in the request and at the time were unable to send the consumer data', 'woocommerce' ) ); + } + + return true; + } + + /** + * Handle auth requests. + * + * @since 2.4.0 + */ + public function handle_auth_requests() { + global $wp; + + if ( ! empty( $_GET['wc-auth-version'] ) ) { + $wp->query_vars['wc-auth-version'] = $_GET['wc-auth-version']; + } + + if ( ! empty( $_GET['wc-auth-route'] ) ) { + $wp->query_vars['wc-auth-route'] = $_GET['wc-auth-route']; + } + + // wc-auth endpoint requests + if ( ! empty( $wp->query_vars['wc-auth-version'] ) && ! empty( $wp->query_vars['wc-auth-route'] ) ) { + $this->auth_endpoint( $wp->query_vars['wc-auth-route'] ); + } + } + + /** + * Auth endpoint. + * + * @since 2.4.0 + * + * @param string $route + */ + protected function auth_endpoint( $route ) { + ob_start(); + + $consumer_data = array(); + + try { + if ( 'yes' !== get_option( 'woocommerce_api_enabled' ) ) { + throw new Exception( __( 'API disabled!', 'woocommerce' ) ); + } + + $route = strtolower( wc_clean( $route ) ); + $this->make_validation(); + + // Login endpoint + if ( 'login' == $route && ! is_user_logged_in() ) { + wc_get_template( 'auth/form-login.php', array( + 'app_name' => $_REQUEST['app_name'], + 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ), + 'redirect_url' => $this->build_url( $_REQUEST, 'authorize' ), + ) ); + + exit; + + // Redirect with user is logged in + } elseif ( 'login' == $route && is_user_logged_in() ) { + wp_redirect( esc_url_raw( $this->build_url( $_REQUEST, 'authorize' ) ) ); + exit; + + // Redirect with user is not logged in and trying to access the authorize endpoint + } elseif ( 'authorize' == $route && ! is_user_logged_in() ) { + wp_redirect( esc_url_raw( $this->build_url( $_REQUEST, 'login' ) ) ); + exit; + + // Authorize endpoint + } elseif ( 'authorize' == $route && current_user_can( 'manage_woocommerce' ) ) { + wc_get_template( 'auth/form-grant-access.php', array( + 'app_name' => $_REQUEST['app_name'], + 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ), + 'scope' => $this->get_i18n_scope( wc_clean( $_REQUEST['scope'] ) ), + 'permissions' => $this->get_permissions_in_scope( wc_clean( $_REQUEST['scope'] ) ), + 'granted_url' => wp_nonce_url( $this->build_url( $_REQUEST, 'access_granted' ), 'wc_auth_grant_access', 'wc_auth_nonce' ), + 'logout_url' => wp_logout_url( $this->build_url( $_REQUEST, 'login' ) ), + 'user' => wp_get_current_user(), + ) ); + exit; + + // Granted access endpoint + } elseif ( 'access_granted' == $route && current_user_can( 'manage_woocommerce' ) ) { + if ( ! isset( $_GET['wc_auth_nonce'] ) || ! wp_verify_nonce( $_GET['wc_auth_nonce'], 'wc_auth_grant_access' ) ) { + throw new Exception( __( 'Invalid nonce verification', 'woocommerce' ) ); + } + + $consumer_data = $this->create_keys( $_REQUEST['app_name'], $_REQUEST['user_id'], $_REQUEST['scope'] ); + $response = $this->post_consumer_data( $consumer_data, $this->get_formatted_url( $_REQUEST['callback_url'] ) ); + + if ( $response ) { + wp_redirect( esc_url_raw( add_query_arg( array( 'success' => 1, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ) ) ); + exit; + } + } else { + throw new Exception( __( 'You do not have permissions to access this page!', 'woocommerce' ) ); + } + } catch ( Exception $e ) { + $this->maybe_delete_key( $consumer_data ); + + /* translators: %s: error messase */ + wp_die( sprintf( __( 'Error: %s.', 'woocommerce' ), $e->getMessage() ), __( 'Access denied', 'woocommerce' ), array( 'response' => 401 ) ); + } + } + + /** + * Maybe delete key. + * + * @since 2.4.0 + * + * @param array $key + */ + private function maybe_delete_key( $key ) { + global $wpdb; + + if ( isset( $key['key_id'] ) ) { + $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key['key_id'] ), array( '%d' ) ); + } + } +} +new WC_Auth(); diff --git a/includes/class-wc-autoloader.php b/includes/class-wc-autoloader.php new file mode 100644 index 00000000000..b6c7416a5ea --- /dev/null +++ b/includes/class-wc-autoloader.php @@ -0,0 +1,100 @@ +include_path = untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) ) . '/includes/'; + } + + /** + * Take a class name and turn it into a file name. + * + * @param string $class + * @return string + */ + private function get_file_name_from_class( $class ) { + return 'class-' . str_replace( '_', '-', $class ) . '.php'; + } + + /** + * Include a class file. + * + * @param string $path + * @return bool successful or not + */ + private function load_file( $path ) { + if ( $path && is_readable( $path ) ) { + include_once( $path ); + return true; + } + return false; + } + + /** + * Auto-load WC classes on demand to reduce memory consumption. + * + * @param string $class + */ + public function autoload( $class ) { + $class = strtolower( $class ); + + if ( 0 !== strpos( $class, 'wc_' ) ) { + return; + } + + $file = $this->get_file_name_from_class( $class ); + $path = ''; + + if ( strpos( $class, 'wc_addons_gateway_' ) === 0 ) { + $path = $this->include_path . 'gateways/' . substr( str_replace( '_', '-', $class ), 18 ) . '/'; + } elseif ( strpos( $class, 'wc_gateway_' ) === 0 ) { + $path = $this->include_path . 'gateways/' . substr( str_replace( '_', '-', $class ), 11 ) . '/'; + } elseif ( strpos( $class, 'wc_shipping_' ) === 0 ) { + $path = $this->include_path . 'shipping/' . substr( str_replace( '_', '-', $class ), 12 ) . '/'; + } elseif ( strpos( $class, 'wc_shortcode_' ) === 0 ) { + $path = $this->include_path . 'shortcodes/'; + } elseif ( strpos( $class, 'wc_meta_box' ) === 0 ) { + $path = $this->include_path . 'admin/meta-boxes/'; + } elseif ( strpos( $class, 'wc_admin' ) === 0 ) { + $path = $this->include_path . 'admin/'; + } elseif ( strpos( $class, 'wc_payment_token_' ) === 0 ) { + $path = $this->include_path . 'payment-tokens/'; + } elseif ( strpos( $class, 'wc_log_handler_' ) === 0 ) { + $path = $this->include_path . 'log-handlers/'; + } + + if ( empty( $path ) || ! $this->load_file( $path . $file ) ) { + $this->load_file( $this->include_path . $file ); + } + } +} + +new WC_Autoloader(); diff --git a/includes/class-wc-background-emailer.php b/includes/class-wc-background-emailer.php new file mode 100644 index 00000000000..332cae0b30a --- /dev/null +++ b/includes/class-wc-background-emailer.php @@ -0,0 +1,162 @@ +cron_hook_identifier ) ) { + wp_schedule_event( time() + 10, $this->cron_interval_identifier, $this->cron_hook_identifier ); + } + } + + /** + * Task + * + * Override this method to perform any actions required on each + * queue item. Return the modified item for further processing + * in the next pass through. Or, return false to remove the + * item from the queue. + * + * @param array $callback Update callback function + * @return mixed + */ + protected function task( $callback ) { + if ( isset( $callback['filter'], $callback['args'] ) ) { + try { + WC_Emails::send_queued_transactional_email( $callback['filter'], $callback['args'] ); + } catch ( Exception $e ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + trigger_error( 'Transactional email triggered fatal error for callback ' . $callback['filter'], E_USER_WARNING ); + } + } + } + return false; + } + + /** + * Save and run queue. + */ + public function dispatch_queue() { + if ( ! empty( $this->data ) ) { + $this->save()->dispatch(); + } + } + + /** + * Get post args + * + * @return array + */ + protected function get_post_args() { + if ( property_exists( $this, 'post_args' ) ) { + return $this->post_args; + } + + // Pass cookies through with the request so nonces function. + $cookies = array(); + + foreach ( $_COOKIE as $name => $value ) { + if ( 'PHPSESSID' === $name ) { + continue; + } + $cookies[] = new WP_Http_Cookie( array( 'name' => $name, 'value' => $value ) ); + } + + return array( + 'timeout' => 0.01, + 'blocking' => false, + 'body' => $this->data, + 'cookies' => $cookies, + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ); + } + + /** + * Handle + * + * Pass each queue item to the task handler, while remaining + * within server memory and time limit constraints. + */ + protected function handle() { + $this->lock_process(); + + do { + $batch = $this->get_batch(); + + if ( empty( $batch->data ) ) { + break; + } + + foreach ( $batch->data as $key => $value ) { + $task = $this->task( $value ); + + if ( false !== $task ) { + $batch->data[ $key ] = $task; + } else { + unset( $batch->data[ $key ] ); + } + + // Update batch before sending more to prevent duplicate email possibility. + $this->update( $batch->key, $batch->data ); + + if ( $this->time_exceeded() || $this->memory_exceeded() ) { + // Batch limits reached. + break; + } + } + if ( empty( $batch->data ) ) { + $this->delete( $batch->key ); + } + } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); + + $this->unlock_process(); + + // Start next batch or complete process. + if ( ! $this->is_queue_empty() ) { + $this->dispatch(); + } else { + $this->complete(); + } + } +} diff --git a/includes/class-wc-background-updater.php b/includes/class-wc-background-updater.php new file mode 100644 index 00000000000..0eec0045ff2 --- /dev/null +++ b/includes/class-wc-background-updater.php @@ -0,0 +1,134 @@ +error( + sprintf( 'Unable to dispatch WooCommerce updater: %s', $dispatched->get_error_message() ), + array( 'source' => 'wc_db_updates' ) + ); + } + } + + /** + * Handle cron healthcheck + * + * Restart the background process if not already running + * and data exists in the queue. + */ + public function handle_cron_healthcheck() { + if ( $this->is_process_running() ) { + // Background process already running. + return; + } + + if ( $this->is_queue_empty() ) { + // No data to process. + $this->clear_scheduled_event(); + return; + } + + $this->handle(); + } + + /** + * Schedule fallback event. + */ + protected function schedule_event() { + if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { + wp_schedule_event( time() + 10, $this->cron_interval_identifier, $this->cron_hook_identifier ); + } + } + + /** + * Is the updater running? + * @return boolean + */ + public function is_updating() { + return false === $this->is_queue_empty(); + } + + /** + * Task + * + * Override this method to perform any actions required on each + * queue item. Return the modified item for further processing + * in the next pass through. Or, return false to remove the + * item from the queue. + * + * @param string $callback Update callback function + * @return mixed + */ + protected function task( $callback ) { + if ( ! defined( 'WC_UPDATING' ) ) { + define( 'WC_UPDATING', true ); + } + + $logger = wc_get_logger(); + + include_once( dirname( __FILE__ ) . '/wc-update-functions.php' ); + + if ( is_callable( $callback ) ) { + $logger->info( sprintf( 'Running %s callback', $callback ), array( 'source' => 'wc_db_updates' ) ); + call_user_func( $callback ); + $logger->info( sprintf( 'Finished %s callback', $callback ), array( 'source' => 'wc_db_updates' ) ); + } else { + $logger->notice( sprintf( 'Could not find %s callback', $callback ), array( 'source' => 'wc_db_updates' ) ); + } + + return false; + } + + /** + * Complete + * + * Override if applicable, but ensure that the below actions are + * performed, or, call parent::complete(). + */ + protected function complete() { + $logger = wc_get_logger(); + $logger->info( 'Data update complete', array( 'source' => 'wc_db_updates' ) ); + WC_Install::update_db_version(); + parent::complete(); + } +} diff --git a/includes/class-wc-breadcrumb.php b/includes/class-wc-breadcrumb.php new file mode 100644 index 00000000000..2950012fd83 --- /dev/null +++ b/includes/class-wc-breadcrumb.php @@ -0,0 +1,352 @@ +crumbs[] = array( + strip_tags( $name ), + $link, + ); + } + + /** + * Reset crumbs. + */ + public function reset() { + $this->crumbs = array(); + } + + /** + * Get the breadcrumb. + * + * @return array + */ + public function get_breadcrumb() { + return apply_filters( 'woocommerce_get_breadcrumb', $this->crumbs, $this ); + } + + /** + * Generate breadcrumb trail. + * + * @return array of breadcrumbs + */ + public function generate() { + $conditionals = array( + 'is_home', + 'is_404', + 'is_attachment', + 'is_single', + 'is_product_category', + 'is_product_tag', + 'is_shop', + 'is_page', + 'is_post_type_archive', + 'is_category', + 'is_tag', + 'is_author', + 'is_date', + 'is_tax', + ); + + if ( ( ! is_front_page() && ! ( is_post_type_archive() && intval( get_option( 'page_on_front' ) ) === wc_get_page_id( 'shop' ) ) ) || is_paged() ) { + foreach ( $conditionals as $conditional ) { + if ( call_user_func( $conditional ) ) { + call_user_func( array( $this, 'add_crumbs_' . substr( $conditional, 3 ) ) ); + break; + } + } + + $this->search_trail(); + $this->paged_trail(); + + return $this->get_breadcrumb(); + } + + return array(); + } + + /** + * Prepend the shop page to shop breadcrumbs. + */ + private function prepend_shop_page() { + $permalinks = wc_get_permalink_structure(); + $shop_page_id = wc_get_page_id( 'shop' ); + $shop_page = get_post( $shop_page_id ); + + // If permalinks contain the shop page in the URI prepend the breadcrumb with shop + if ( $shop_page_id && $shop_page && isset( $permalinks['product_base'] ) && strstr( $permalinks['product_base'], '/' . $shop_page->post_name ) && get_option( 'page_on_front' ) != $shop_page_id ) { + $this->add_crumb( get_the_title( $shop_page ), get_permalink( $shop_page ) ); + } + } + + /** + * is home trail. + */ + private function add_crumbs_home() { + $this->add_crumb( single_post_title( '', false ) ); + } + + /** + * 404 trail. + */ + private function add_crumbs_404() { + $this->add_crumb( __( 'Error 404', 'woocommerce' ) ); + } + + /** + * attachment trail. + */ + private function add_crumbs_attachment() { + global $post; + + $this->add_crumbs_single( $post->post_parent, get_permalink( $post->post_parent ) ); + $this->add_crumb( get_the_title(), get_permalink() ); + } + + /** + * Single post trail. + * + * @param int $post_id + * @param string $permalink + */ + private function add_crumbs_single( $post_id = 0, $permalink = '' ) { + if ( ! $post_id ) { + global $post; + } else { + $post = get_post( $post_id ); + } + + if ( 'product' === get_post_type( $post ) ) { + $this->prepend_shop_page(); + if ( $terms = wc_get_product_terms( $post->ID, 'product_cat', array( 'orderby' => 'parent', 'order' => 'DESC' ) ) ) { + $main_term = apply_filters( 'woocommerce_breadcrumb_main_term', $terms[0], $terms ); + $this->term_ancestors( $main_term->term_id, 'product_cat' ); + $this->add_crumb( $main_term->name, get_term_link( $main_term ) ); + } + } elseif ( 'post' != get_post_type( $post ) ) { + $post_type = get_post_type_object( get_post_type( $post ) ); + $this->add_crumb( $post_type->labels->singular_name, get_post_type_archive_link( get_post_type( $post ) ) ); + } else { + $cat = current( get_the_category( $post ) ); + if ( $cat ) { + $this->term_ancestors( $cat->term_id, 'post_category' ); + $this->add_crumb( $cat->name, get_term_link( $cat ) ); + } + } + + $this->add_crumb( get_the_title( $post ), $permalink ); + } + + /** + * Page trail. + */ + private function add_crumbs_page() { + global $post; + + if ( $post->post_parent ) { + $parent_crumbs = array(); + $parent_id = $post->post_parent; + + while ( $parent_id ) { + $page = get_post( $parent_id ); + $parent_id = $page->post_parent; + $parent_crumbs[] = array( get_the_title( $page->ID ), get_permalink( $page->ID ) ); + } + + $parent_crumbs = array_reverse( $parent_crumbs ); + + foreach ( $parent_crumbs as $crumb ) { + $this->add_crumb( $crumb[0], $crumb[1] ); + } + } + + $this->add_crumb( get_the_title(), get_permalink() ); + $this->endpoint_trail(); + } + + /** + * Product category trail. + */ + private function add_crumbs_product_category() { + $current_term = $GLOBALS['wp_query']->get_queried_object(); + + $this->prepend_shop_page(); + $this->term_ancestors( $current_term->term_id, 'product_cat' ); + $this->add_crumb( $current_term->name ); + } + + /** + * Product tag trail. + */ + private function add_crumbs_product_tag() { + $current_term = $GLOBALS['wp_query']->get_queried_object(); + + $this->prepend_shop_page(); + $this->add_crumb( sprintf( __( 'Products tagged “%s”', 'woocommerce' ), $current_term->name ) ); + } + + /** + * Shop breadcrumb. + */ + private function add_crumbs_shop() { + if ( get_option( 'page_on_front' ) == wc_get_page_id( 'shop' ) ) { + return; + } + + $_name = wc_get_page_id( 'shop' ) ? get_the_title( wc_get_page_id( 'shop' ) ) : ''; + + if ( ! $_name ) { + $product_post_type = get_post_type_object( 'product' ); + $_name = $product_post_type->labels->singular_name; + } + + $this->add_crumb( $_name, get_post_type_archive_link( 'product' ) ); + } + + /** + * Post type archive trail. + */ + private function add_crumbs_post_type_archive() { + $post_type = get_post_type_object( get_post_type() ); + + if ( $post_type ) { + $this->add_crumb( $post_type->labels->singular_name, get_post_type_archive_link( get_post_type() ) ); + } + } + + /** + * Category trail. + */ + private function add_crumbs_category() { + $this_category = get_category( $GLOBALS['wp_query']->get_queried_object() ); + + if ( 0 != $this_category->parent ) { + $this->term_ancestors( $this_category->parent, 'post_category' ); + $this->add_crumb( $this_category->name, get_category_link( $this_category->term_id ) ); + } + + $this->add_crumb( single_cat_title( '', false ), get_category_link( $this_category->term_id ) ); + } + + /** + * Tag trail. + */ + private function add_crumbs_tag() { + $queried_object = $GLOBALS['wp_query']->get_queried_object(); + $this->add_crumb( sprintf( __( 'Posts tagged “%s”', 'woocommerce' ), single_tag_title( '', false ) ), get_tag_link( $queried_object->term_id ) ); + } + + /** + * Add crumbs for date based archives. + */ + private function add_crumbs_date() { + if ( is_year() || is_month() || is_day() ) { + $this->add_crumb( get_the_time( 'Y' ), get_year_link( get_the_time( 'Y' ) ) ); + } + if ( is_month() || is_day() ) { + $this->add_crumb( get_the_time( 'F' ), get_month_link( get_the_time( 'Y' ), get_the_time( 'm' ) ) ); + } + if ( is_day() ) { + $this->add_crumb( get_the_time( 'd' ) ); + } + } + + /** + * Add crumbs for taxonomies + */ + private function add_crumbs_tax() { + $this_term = $GLOBALS['wp_query']->get_queried_object(); + $taxonomy = get_taxonomy( $this_term->taxonomy ); + + $this->add_crumb( $taxonomy->labels->name ); + + if ( 0 != $this_term->parent ) { + $this->term_ancestors( $this_term->term_id, $this_term->taxonomy ); + } + + $this->add_crumb( single_term_title( '', false ), get_term_link( $this_term->term_id, $this_term->taxonomy ) ); + } + + /** + * Add a breadcrumb for author archives. + */ + private function add_crumbs_author() { + global $author; + + $userdata = get_userdata( $author ); + $this->add_crumb( sprintf( __( 'Author: %s', 'woocommerce' ), $userdata->display_name ) ); + } + + /** + * Add crumbs for a term. + * + * @param int $term_id + * @param string $taxonomy + */ + private function term_ancestors( $term_id, $taxonomy ) { + $ancestors = get_ancestors( $term_id, $taxonomy ); + $ancestors = array_reverse( $ancestors ); + + foreach ( $ancestors as $ancestor ) { + $ancestor = get_term( $ancestor, $taxonomy ); + + if ( ! is_wp_error( $ancestor ) && $ancestor ) { + $this->add_crumb( $ancestor->name, get_term_link( $ancestor ) ); + } + } + } + + /** + * Endpoints. + */ + private function endpoint_trail() { + // Is an endpoint showing? + if ( is_wc_endpoint_url() && ( $endpoint = WC()->query->get_current_endpoint() ) && ( $endpoint_title = WC()->query->get_endpoint_title( $endpoint ) ) ) { + $this->add_crumb( $endpoint_title ); + } + } + + /** + * Add a breadcrumb for search results. + */ + private function search_trail() { + if ( is_search() ) { + $this->add_crumb( sprintf( __( 'Search results for “%s”', 'woocommerce' ), get_search_query() ), remove_query_arg( 'paged' ) ); + } + } + + /** + * Add a breadcrumb for pagination. + */ + private function paged_trail() { + if ( get_query_var( 'paged' ) ) { + $this->add_crumb( sprintf( __( 'Page %d', 'woocommerce' ), get_query_var( 'paged' ) ) ); + } + } +} diff --git a/includes/class-wc-cache-helper.php b/includes/class-wc-cache-helper.php index 613bfd73353..790b3fdd907 100644 --- a/includes/class-wc-cache-helper.php +++ b/includes/class-wc-cache-helper.php @@ -16,29 +16,100 @@ if ( ! defined( 'ABSPATH' ) ) { class WC_Cache_Helper { /** - * Hook in methods + * Hook in methods. */ public static function init() { - add_action( 'before_woocommerce_init', array( __CLASS__, 'prevent_caching' ) ); + add_action( 'template_redirect', array( __CLASS__, 'geolocation_ajax_redirect' ) ); + add_action( 'wp', array( __CLASS__, 'prevent_caching' ) ); add_action( 'admin_notices', array( __CLASS__, 'notices' ) ); + add_action( 'delete_version_transients', array( __CLASS__, 'delete_version_transients' ) ); } /** - * Get transient version + * Get prefix for use with wp_cache_set. Allows all cache in a group to be invalidated at once. + * @param string $group + * @return string + */ + public static function get_cache_prefix( $group ) { + // Get cache key - uses cache key wc_orders_cache_prefix to invalidate when needed + $prefix = wp_cache_get( 'wc_' . $group . '_cache_prefix', $group ); + + if ( false === $prefix ) { + $prefix = 1; + wp_cache_set( 'wc_' . $group . '_cache_prefix', $prefix, $group ); + } + + return 'wc_cache_' . $prefix . '_'; + } + + /** + * Increment group cache prefix (invalidates cache). + * @param string $group + */ + public static function incr_cache_prefix( $group ) { + wp_cache_incr( 'wc_' . $group . '_cache_prefix', 1, $group ); + } + + /** + * Get a hash of the customer location. + * @return string + */ + public static function geolocation_ajax_get_location_hash() { + $customer = new WC_Customer( 0, true ); + $location = array(); + $location['country'] = $customer->get_billing_country(); + $location['state'] = $customer->get_billing_state(); + $location['postcode'] = $customer->get_billing_postcode(); + $location['city'] = $customer->get_billing_city(); + return substr( md5( implode( '', $location ) ), 0, 12 ); + } + + /** + * When using geolocation via ajax, to bust cache, redirect if the location hash does not equal the querystring. * - * When using transients with unpredictable names, e.g. those containing an md5 - * hash in the name, we need a way to invalidate them all at once. + * This prevents caching of the wrong data for this request. + */ + public static function geolocation_ajax_redirect() { + if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) && ! is_checkout() && ! is_cart() && ! is_account_page() && ! is_ajax() && empty( $_POST ) ) { + $location_hash = self::geolocation_ajax_get_location_hash(); + $current_hash = isset( $_GET['v'] ) ? wc_clean( $_GET['v'] ) : ''; + if ( empty( $current_hash ) || $current_hash !== $location_hash ) { + global $wp; + + $redirect_url = trailingslashit( home_url( $wp->request ) ); + + if ( ! empty( $_SERVER['QUERY_STRING'] ) ) { + $redirect_url = add_query_arg( $_SERVER['QUERY_STRING'], '', $redirect_url ); + } + + if ( ! get_option( 'permalink_structure' ) ) { + $redirect_url = add_query_arg( $wp->query_string, '', $redirect_url ); + } + + $redirect_url = add_query_arg( 'v', $location_hash, remove_query_arg( 'v', $redirect_url ) ); + + wp_safe_redirect( esc_url_raw( $redirect_url ), 307 ); + exit; + } + } + } + + /** + * Get transient version. * - * When using default WP transients we're able to do this with a DB query to + * When using transients with unpredictable names, e.g. those containing an md5. + * hash in the name, we need a way to invalidate them all at once. + * + * When using default WP transients we're able to do this with a DB query to. * delete transients manually. * - * With external cache however, this isn't possible. Instead, this function is used - * to append a unique string (based on time()) to each transient. When transients + * With external cache however, this isn't possible. Instead, this function is used. + * to append a unique string (based on time()) to each transient. When transients. * are invalidated, the transient version will increment and data will be regenerated. * - * Raised in issue https://github.com/woothemes/woocommerce/issues/5777 - * Adapted from ideas in http://tollmanz.com/invalidation-schemes/ - * + * Raised in issue https://github.com/woocommerce/woocommerce/issues/5777. + * Adapted from ideas in http://tollmanz.com/invalidation-schemes/. + * * @param string $group Name for the group of transients we need to invalidate * @param boolean $refresh true to force a new version * @return string transient version based on time(), 10 digits @@ -48,93 +119,83 @@ class WC_Cache_Helper { $transient_value = get_transient( $transient_name ); if ( false === $transient_value || true === $refresh ) { - $transient_value = time(); - set_transient( $transient_name, $transient_value ); + self::delete_version_transients( $transient_value ); + set_transient( $transient_name, $transient_value = time() ); } return $transient_value; } /** - * Prevent caching on dynamic pages. + * When the transient version increases, this is used to remove all past transients to avoid filling the DB. * - * @access public - * @return void + * Note; this only works on transients appended with the transient version, and when object caching is not being used. + * + * @since 2.3.10 + * + * @param string $version + */ + public static function delete_version_transients( $version = '' ) { + if ( ! wp_using_ext_object_cache() && ! empty( $version ) ) { + global $wpdb; + + $limit = apply_filters( 'woocommerce_delete_version_transients_limit', 1000 ); + $affected = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s ORDER BY option_id LIMIT %d;", "\_transient\_%" . $version, $limit ) ); + + // If affected rows is equal to limit, there are more rows to delete. Delete in 10 secs. + if ( $affected === $limit ) { + wp_schedule_single_event( time() + 10, 'delete_version_transients', array( $version ) ); + } + } + } + + /** + * Prevent caching on dynamic pages. */ public static function prevent_caching() { - if ( false === ( $wc_page_uris = get_transient( 'woocommerce_cache_excluded_uris' ) ) ) { - - if ( wc_get_page_id( 'cart' ) < 1 || wc_get_page_id( 'checkout' ) < 1 || wc_get_page_id( 'myaccount' ) < 1 ) - return; - - $wc_page_uris = array(); - - // Exclude querystring when using page ID - $wc_page_uris[] = 'p=' . wc_get_page_id( 'cart' ); - $wc_page_uris[] = 'p=' . wc_get_page_id( 'checkout' ); - $wc_page_uris[] = 'p=' . wc_get_page_id( 'myaccount' ); - - // Exclude permalinks - $cart_page = get_post( wc_get_page_id( 'cart' ) ); - $checkout_page = get_post( wc_get_page_id( 'checkout' ) ); - $account_page = get_post( wc_get_page_id( 'myaccount' ) ); - - if ( ! is_null( $cart_page ) ) - $wc_page_uris[] = '/' . $cart_page->post_name; - if ( ! is_null( $checkout_page ) ) - $wc_page_uris[] = '/' . $checkout_page->post_name; - if ( ! is_null( $account_page ) ) - $wc_page_uris[] = '/' . $account_page->post_name; - - set_transient( 'woocommerce_cache_excluded_uris', $wc_page_uris ); + if ( ! is_blog_installed() ) { + return; } + $page_ids = array_filter( array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ), wc_get_page_id( 'myaccount' ) ) ); + $current_page_id = get_queried_object_id(); - if ( is_array( $wc_page_uris ) ) { - foreach( $wc_page_uris as $uri ) - if ( strstr( $_SERVER['REQUEST_URI'], $uri ) ) { - self::nocache(); - break; - } + if ( isset( $_GET['download_file'] ) || in_array( $current_page_id, $page_ids ) ) { + self::nocache(); } } /** * Set nocache constants and headers. - * * @access private - * @return void */ private static function nocache() { - if ( ! defined( 'DONOTCACHEPAGE' ) ) - define( "DONOTCACHEPAGE", "true" ); - - if ( ! defined( 'DONOTCACHEOBJECT' ) ) - define( "DONOTCACHEOBJECT", "true" ); - - if ( ! defined( 'DONOTCACHEDB' ) ) - define( "DONOTCACHEDB", "true" ); - + if ( ! defined( 'DONOTCACHEPAGE' ) ) { + define( "DONOTCACHEPAGE", true ); + } + if ( ! defined( 'DONOTCACHEOBJECT' ) ) { + define( "DONOTCACHEOBJECT", true ); + } + if ( ! defined( 'DONOTCACHEDB' ) ) { + define( "DONOTCACHEDB", true ); + } nocache_headers(); } /** * notices function. - * - * @access public - * @return void */ public static function notices() { if ( ! function_exists( 'w3tc_pgcache_flush' ) || ! function_exists( 'w3_instance' ) ) { return; } - $config = w3_instance('W3_Config'); + $config = w3_instance( 'W3_Config' ); $enabled = $config->get_integer( 'dbcache.enabled' ); - $settings = $config->get_array( 'dbcache.reject.sql' ); + $settings = array_map( 'trim', $config->get_array( 'dbcache.reject.sql' ) ); if ( $enabled && ! in_array( '_wc_session_', $settings ) ) { ?>
    -

    database caching to work with WooCommerce you must add _wc_session_ to the "Ignored Query Strings" option in W3 Total Cache settings here.', 'woocommerce' ), admin_url( 'admin.php?page=w3tc_dbcache' ) ); ?>

    +

    database caching to work with WooCommerce you must add %1$s to the "Ignored Query Strings" option in W3 Total Cache settings.', 'woocommerce' ), '_wc_session_', admin_url( 'admin.php?page=w3tc_dbcache' ) ); ?>

    0, + 'total' => 0, + 'subtotal' => 0, + 'subtotal_ex_tax' => 0, + 'tax_total' => 0, + 'taxes' => array(), + 'shipping_taxes' => array(), + 'discount_cart' => 0, + 'discount_cart_tax' => 0, + 'shipping_total' => 0, + 'shipping_tax_total' => 0, + 'coupon_discount_amounts' => array(), + 'coupon_discount_tax_amounts' => array(), + 'fee_total' => 0, + 'fees' => array(), + ); - /** @var array cart_session_data */ - public $cart_session_data = array(); - - /** @var array An array of fees. */ + /** + * An array of fees. + * + * @var array + */ public $fees = array(); /** * Constructor for the cart class. Loads options and hooks in the init method. - * - * @access public - * @return void */ public function __construct() { - $this->tax = new WC_Tax(); - $this->prices_include_tax = get_option( 'woocommerce_prices_include_tax' ) == 'yes'; - $this->round_at_subtotal = get_option('woocommerce_tax_round_at_subtotal') == 'yes'; - $this->tax_display_cart = get_option( 'woocommerce_tax_display_cart' ); - $this->dp = absint( get_option( 'woocommerce_price_num_decimals' ) ); - $this->display_totals_ex_tax = $this->tax_display_cart == 'excl'; - $this->display_cart_ex_tax = $this->tax_display_cart == 'excl'; - - // Array of data the cart calculates and stores in the session with defaults - $this->cart_session_data = array( - 'cart_contents_total' => 0, - 'cart_contents_weight' => 0, - 'cart_contents_count' => 0, - 'cart_contents_tax' => 0, - 'total' => 0, - 'subtotal' => 0, - 'subtotal_ex_tax' => 0, - 'tax_total' => 0, - 'taxes' => array(), - 'shipping_taxes' => array(), - 'discount_cart' => 0, - 'discount_total' => 0, - 'shipping_total' => 0, - 'shipping_tax_total' => 0, - 'coupon_discount_amounts' => array(), - ); - - add_action( 'init', array( $this, 'init' ), 5 ); // Get cart on init + add_action( 'wp_loaded', array( $this, 'init' ) ); // Get cart after WP and plugins are loaded. add_action( 'wp', array( $this, 'maybe_set_cart_cookies' ), 99 ); // Set cookies - add_action( 'shutdown', array( $this, 'maybe_set_cart_cookies' ), 99 ); // Set cookies + add_action( 'shutdown', array( $this, 'maybe_set_cart_cookies' ), 0 ); // Set cookies before shutdown and ob flushing + add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 ); + add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 ); } - /** + /** + * Auto-load in-accessible properties on demand. + * + * @param mixed $key + * @return mixed + */ + public function __get( $key ) { + switch ( $key ) { + case 'prices_include_tax' : + return wc_prices_include_tax(); + break; + case 'round_at_subtotal' : + return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ); + break; + case 'tax_display_cart' : + return get_option( 'woocommerce_tax_display_cart' ); + break; + case 'dp' : + return wc_get_price_decimals(); + break; + case 'display_totals_ex_tax' : + case 'display_cart_ex_tax' : + return 'excl' === $this->tax_display_cart; + break; + case 'cart_contents_weight' : + return $this->get_cart_contents_weight(); + break; + case 'cart_contents_count' : + return $this->get_cart_contents_count(); + break; + case 'tax' : + wc_deprecated_argument( 'WC_Cart->tax', '2.3', 'Use WC_Tax:: directly' ); + $this->tax = new WC_Tax(); + return $this->tax; + case 'discount_total': + wc_deprecated_argument( 'WC_Cart->discount_total', '2.3', 'After tax coupons are no longer supported. For more information see: https://woocommerce.wordpress.com/2014/12/upcoming-coupon-changes-in-woocommerce-2-3/' ); + return 0; + } + } + + /** * Loads the cart data from the PHP session during WordPress init and hooks in other methods. - * - * @access public - * @return void - */ - public function init() { + */ + public function init() { $this->get_cart_from_session(); add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 ); add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_coupons' ), 1 ); add_action( 'woocommerce_after_checkout_validation', array( $this, 'check_customer_coupons' ), 1 ); - } + } - /** - * Will set cart cookies if needed, once, during WP hook - */ - public function maybe_set_cart_cookies() { - if ( ! headers_sent() ) { - if ( sizeof( $this->cart_contents ) > 0 ) { - $this->set_cart_cookies( true ); - } elseif ( isset( $_COOKIE['woocommerce_items_in_cart'] ) ) { - $this->set_cart_cookies( false ); - } - } - } + /** + * Will set cart cookies if needed, once, during WP hook. + */ + public function maybe_set_cart_cookies() { + if ( ! headers_sent() && did_action( 'wp_loaded' ) ) { + if ( ! $this->is_empty() ) { + $this->set_cart_cookies( true ); + } elseif ( isset( $_COOKIE['woocommerce_items_in_cart'] ) ) { + $this->set_cart_cookies( false ); + } + } + } /** * Set cart hash cookie and items in cart. * * @access private * @param bool $set (default: true) - * @return void */ private function set_cart_cookies( $set = true ) { if ( $set ) { wc_setcookie( 'woocommerce_items_in_cart', 1 ); - wc_setcookie( 'woocommerce_cart_hash', md5( json_encode( $this->get_cart() ) ) ); + wc_setcookie( 'woocommerce_cart_hash', md5( json_encode( $this->get_cart_for_session() ) ) ); } elseif ( isset( $_COOKIE['woocommerce_items_in_cart'] ) ) { - wc_setcookie( 'woocommerce_items_in_cart', 0, time() - 3600 ); - wc_setcookie( 'woocommerce_cart_hash', '', time() - 3600 ); + wc_setcookie( 'woocommerce_items_in_cart', 0, time() - HOUR_IN_SECONDS ); + wc_setcookie( 'woocommerce_cart_hash', '', time() - HOUR_IN_SECONDS ); } do_action( 'woocommerce_set_cart_cookies', $set ); } - /*-----------------------------------------------------------------------------------*/ - /* Cart Session Handling */ - /*-----------------------------------------------------------------------------------*/ + /* + /* Cart Session Handling + */ + + /** + * Get the cart data from the PHP session and store it in class variables. + */ + public function get_cart_from_session() { + // Load cart session data from session + foreach ( $this->cart_session_data as $key => $default ) { + $this->$key = WC()->session->get( $key, $default ); + } + + $update_cart_session = false; + $this->removed_cart_contents = array_filter( WC()->session->get( 'removed_cart_contents', array() ) ); + $this->applied_coupons = array_filter( WC()->session->get( 'applied_coupons', array() ) ); /** - * Get the cart data from the PHP session and store it in class variables. - * - * @access public - * @return void + * Load the cart object. This defaults to the persistent cart if null. */ - public function get_cart_from_session() { + $cart = WC()->session->get( 'cart', null ); - // Load cart session data from session - foreach ( $this->cart_session_data as $key => $default ) { - $this->$key = WC()->session->get( $key, $default ); - } + if ( is_null( $cart ) && ( $saved_cart = get_user_meta( get_current_user_id(), '_woocommerce_persistent_cart_' . get_current_blog_id(), true ) ) ) { + $cart = $saved_cart['cart']; + $update_cart_session = true; + } elseif ( is_null( $cart ) ) { + $cart = array(); + } - // Load coupons - $this->applied_coupons = array_filter( WC()->session->get( 'applied_coupons', array() ) ); + if ( is_array( $cart ) ) { + // Prime meta cache to reduce future queries + update_meta_cache( 'post', wp_list_pluck( $cart, 'product_id' ) ); + update_object_term_cache( wp_list_pluck( $cart, 'product_id' ), 'product' ); - // Load the cart - $cart = WC()->session->get( 'cart', array() ); + foreach ( $cart as $key => $values ) { + $product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] ); - $update_cart_session = false; + if ( ! empty( $product ) && $product->exists() && $values['quantity'] > 0 ) { - if ( is_array( $cart ) ) { - foreach ( $cart as $key => $values ) { - $_product = get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] ); + if ( ! $product->is_purchasable() ) { - if ( ! empty( $_product ) && $_product->exists() && $values['quantity'] > 0 ) { + // Flag to indicate the stored cart should be update + $update_cart_session = true; + /* translators: %s: product name */ + wc_add_notice( sprintf( __( '%s has been removed from your cart because it can no longer be purchased. Please contact us if you need assistance.', 'woocommerce' ), $product->get_name() ), 'error' ); + do_action( 'woocommerce_remove_cart_item_from_session', $key, $values ); - if ( ! $_product->is_purchasable() ) { + } else { - // Flag to indicate the stored cart should be update - $update_cart_session = true; - wc_add_notice( sprintf( __( '%s has been removed from your cart because it can no longer be purchased. Please contact us if you need assistance.', 'woocommerce' ), $_product->get_title() ), 'error' ); + // Put session data into array. Run through filter so other plugins can load their own session data + $session_data = array_merge( $values, array( 'data' => $product ) ); + $this->cart_contents[ $key ] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key ); - } else { - - // Put session data into array. Run through filter so other plugins can load their own session data - $this->cart_contents[ $key ] = apply_filters( 'woocommerce_get_cart_item_from_session', array( - 'product_id' => $values['product_id'], - 'variation_id' => $values['variation_id'], - 'variation' => $values['variation'], - 'quantity' => $values['quantity'], - 'data' => $_product - ), $values, $key ); - - } } } } + } - if ( $update_cart_session ) { - WC()->session->cart = $this->get_cart_for_session(); + // Trigger action + do_action( 'woocommerce_cart_loaded_from_session', $this ); + + if ( $update_cart_session ) { + WC()->session->cart = $this->get_cart_for_session(); + } + + // Queue re-calc if subtotal is not set + if ( ( ! $this->subtotal && ! $this->is_empty() ) || $update_cart_session ) { + $this->calculate_totals(); + } + } + + /** + * Sets the php session data for the cart and coupons. + */ + public function set_session() { + // Set cart and coupon session data + $cart_session = $this->get_cart_for_session(); + + WC()->session->set( 'cart', $cart_session ); + WC()->session->set( 'applied_coupons', $this->applied_coupons ); + WC()->session->set( 'coupon_discount_amounts', $this->coupon_discount_amounts ); + WC()->session->set( 'coupon_discount_tax_amounts', $this->coupon_discount_tax_amounts ); + WC()->session->set( 'removed_cart_contents', $this->removed_cart_contents ); + + foreach ( $this->cart_session_data as $key => $default ) { + WC()->session->set( $key, $this->$key ); + } + + if ( get_current_user_id() ) { + $this->persistent_cart_update(); + } + + do_action( 'woocommerce_cart_updated' ); + } + + /** + * Empties the cart and optionally the persistent cart too. + * + * @param bool $clear_persistent_cart (default: true) + */ + public function empty_cart( $clear_persistent_cart = true ) { + $this->cart_contents = array(); + $this->reset( true ); + + unset( WC()->session->order_awaiting_payment, WC()->session->applied_coupons, WC()->session->coupon_discount_amounts, WC()->session->coupon_discount_tax_amounts, WC()->session->cart ); + + if ( $clear_persistent_cart && get_current_user_id() ) { + $this->persistent_cart_destroy(); + } + + do_action( 'woocommerce_cart_emptied' ); + } + + /* + * Persistent cart handling + */ + + /** + * Save the persistent cart when the cart is updated. + */ + public function persistent_cart_update() { + update_user_meta( get_current_user_id(), '_woocommerce_persistent_cart_' . get_current_blog_id(), array( + 'cart' => WC()->session->get( 'cart' ), + ) ); + } + + /** + * Delete the persistent cart permanently. + */ + public function persistent_cart_destroy() { + delete_user_meta( get_current_user_id(), '_woocommerce_persistent_cart_' . get_current_blog_id() ); + } + + /* + * Cart Data Functions. + */ + + /** + * Coupons enabled function. Filterable. + * + * @deprecated 2.5.0 in favor to wc_coupons_enabled() + * + * @return bool + */ + public function coupons_enabled() { + return wc_coupons_enabled(); + } + + /** + * Get number of items in the cart. + * @return int + */ + public function get_cart_contents_count() { + return apply_filters( 'woocommerce_cart_contents_count', array_sum( wp_list_pluck( $this->get_cart(), 'quantity' ) ) ); + } + + /** + * Get weight of items in the cart. + * @since 2.5.0 + * @return int + */ + public function get_cart_contents_weight() { + $weight = 0; + + foreach ( $this->get_cart() as $cart_item_key => $values ) { + $weight += (float) $values['data']->get_weight() * $values['quantity']; + } + + return apply_filters( 'woocommerce_cart_contents_weight', $weight ); + } + + /** + * Checks if the cart is empty. + * + * @return bool + */ + public function is_empty() { + return 0 === sizeof( $this->get_cart() ); + } + + /** + * Check all cart items for errors. + */ + public function check_cart_items() { + + // Result + $return = true; + + // Check cart item validity + $result = $this->check_cart_item_validity(); + + if ( is_wp_error( $result ) ) { + wc_add_notice( $result->get_error_message(), 'error' ); + $return = false; + } + + // Check item stock + $result = $this->check_cart_item_stock(); + + if ( is_wp_error( $result ) ) { + wc_add_notice( $result->get_error_message(), 'error' ); + $return = false; + } + + return $return; + + } + + /** + * Check cart coupons for errors. + */ + public function check_cart_coupons() { + foreach ( $this->applied_coupons as $code ) { + $coupon = new WC_Coupon( $code ); + + if ( ! $coupon->is_valid() ) { + // Error message + $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_INVALID_REMOVED ); + + // Remove the coupon + $this->remove_coupon( $code ); + + // Flag totals for refresh + WC()->session->set( 'refresh_totals', true ); } + } + } - // Trigger action - do_action( 'woocommerce_cart_loaded_from_session', $this ); + /** + * Get cart items quantities - merged so we can do accurate stock checks on items across multiple lines. + * + * @return array + */ + public function get_cart_item_quantities() { + $quantities = array(); - // Queue re-calc if subtotal is not set - if ( ( ! $this->subtotal && sizeof( $this->cart_contents ) > 0 ) || $update_cart_session ) { - $this->calculate_totals(); + foreach ( $this->get_cart() as $cart_item_key => $values ) { + $product = $values['data']; + $quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $values['quantity'] : $values['quantity']; + } + + return $quantities; + } + + /** + * Looks through cart items and checks the posts are not trashed or deleted. + * + * @return bool|WP_Error + */ + public function check_cart_item_validity() { + $return = true; + + foreach ( $this->get_cart() as $cart_item_key => $values ) { + $product = $values['data']; + + if ( ! $product || ! $product->exists() || 'trash' === $product->get_status() ) { + $this->set_quantity( $cart_item_key, 0 ); + $return = new WP_Error( 'invalid', __( 'An item which is no longer available was removed from your cart.', 'woocommerce' ) ); } } - /** - * Sets the php session data for the cart and coupons. - */ - public function set_session() { - // Set cart and coupon session data - $cart_session = $this->get_cart_for_session(); + return $return; + } - WC()->session->set( 'cart', $cart_session ); - WC()->session->set( 'applied_coupons', $this->applied_coupons ); - WC()->session->set( 'coupon_discount_amounts', $this->coupon_discount_amounts ); + /** + * Looks through the cart to check each item is in stock. If not, add an error. + * + * @return bool|WP_Error + */ + public function check_cart_item_stock() { + global $wpdb; - foreach ( $this->cart_session_data as $key => $default ) { - WC()->session->set( $key, $this->$key ); + $error = new WP_Error(); + $product_qty_in_cart = $this->get_cart_item_quantities(); + + // First stock check loop + foreach ( $this->get_cart() as $cart_item_key => $values ) { + $product = $values['data']; + + /** + * Check stock based on stock-status. + */ + if ( ! $product->is_in_stock() ) { + /* translators: %s: product name */ + $error->add( 'out-of-stock', sprintf( __( 'Sorry, "%s" is not in stock. Please edit your cart and try again. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ) ); + return $error; } - if ( get_current_user_id() ) { - $this->persistent_cart_update(); + if ( ! $product->managing_stock() ) { + continue; } - do_action( 'woocommerce_cart_updated' ); - } - - /** - * Empties the cart and optionally the persistent cart too. - * - * @access public - * @param bool $clear_persistent_cart (default: true) - * @return void - */ - public function empty_cart( $clear_persistent_cart = true ) { - $this->cart_contents = array(); - $this->reset(); - - unset( WC()->session->order_awaiting_payment, WC()->session->applied_coupons, WC()->session->coupon_discount_amounts, WC()->session->cart ); - - if ( $clear_persistent_cart && get_current_user_id() ) { - $this->persistent_cart_destroy(); + /** + * Check stock based on all items in the cart. + */ + if ( ! $product->has_enough_stock( $product_qty_in_cart[ $product->get_stock_managed_by_id() ] ) ) { + /* translators: 1: product name 2: quantity in stock */ + $error->add( 'out-of-stock', sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s in stock). Please edit your cart and try again. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) ); + return $error; } - do_action( 'woocommerce_cart_emptied' ); - } + /** + * Finally consider any held stock, from pending orders. + */ + if ( get_option( 'woocommerce_hold_stock_minutes' ) > 0 && ! $product->backorders_allowed() ) { + $order_id = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0; + $held_stock = $wpdb->get_var( + $wpdb->prepare( " + SELECT SUM( order_item_meta.meta_value ) AS held_qty + FROM {$wpdb->posts} AS posts + 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 posts.post_type IN ( '" . implode( "','", wc_get_order_types() ) . "' ) + AND posts.post_status = 'wc-pending' + AND posts.ID != %d;", + 'variation' === get_post_type( $product->get_stock_managed_by_id() ) ? '_variation_id' : '_product_id', + $product->get_stock_managed_by_id(), + $order_id + ) + ); - /*-----------------------------------------------------------------------------------*/ - /* Persistent cart handling */ - /*-----------------------------------------------------------------------------------*/ - - /** - * Save the persistent cart when the cart is updated. - * - * @access public - * @return void - */ - public function persistent_cart_update() { - update_user_meta( get_current_user_id(), '_woocommerce_persistent_cart', array( - 'cart' => WC()->session->cart, - ) ); - } - - /** - * Delete the persistent cart permanently. - * - * @access public - * @return void - */ - public function persistent_cart_destroy() { - delete_user_meta( get_current_user_id(), '_woocommerce_persistent_cart' ); - } - - /*-----------------------------------------------------------------------------------*/ - /* Cart Data Functions */ - /*-----------------------------------------------------------------------------------*/ - - /** - * Coupons enabled function. Filterable. - * - * @access public - * @return bool - */ - public function coupons_enabled() { - return apply_filters( 'woocommerce_coupons_enabled', get_option( 'woocommerce_enable_coupons' ) == 'yes' ); - } - - /** - * Get number of items in the cart. - * - * @access public - * @return int - */ - public function get_cart_contents_count() { - return apply_filters( 'woocommerce_cart_contents_count', $this->cart_contents_count ); - } - - /** - * Check all cart items for errors. - * - * @access public - * @return void - */ - public function check_cart_items() { - $result = $this->check_cart_item_validity(); - - if ( is_wp_error( $result ) ) { - wc_add_notice( $result->get_error_message(), 'error' ); - } - - // Check item stock - $result = $this->check_cart_item_stock(); - - if ( is_wp_error( $result ) ) { - wc_add_notice( $result->get_error_message(), 'error' ); - } - } - - /** - * Check cart coupons for errors. - * - * @access public - * @return void - */ - public function check_cart_coupons() { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); - - if ( ! $coupon->is_valid() ) { - // Error message - $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_INVALID_REMOVED ); - - // Remove the coupon - $this->remove_coupon( $code ); - - // Flag totals for refresh - WC()->session->set( 'refresh_totals', true ); + if ( $product->get_stock_quantity() < ( $held_stock + $product_qty_in_cart[ $product->get_stock_managed_by_id() ] ) ) { + /* translators: 1: product name 2: minutes */ + $error->add( 'out-of-stock', sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order right now. Please try again in %2$d minutes or edit your cart and try again. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), get_option( 'woocommerce_hold_stock_minutes' ) ) ); + return $error; } } } - /** - * Get cart items quantities - merged so we can do accurate stock checks on items across multiple lines. - * - * @access public - * @return array - */ - public function get_cart_item_quantities() { - $quantities = array(); + return true; + } + /** + * Gets and formats a list of cart item data + variations for display on the frontend. + * + * @param array $cart_item + * @param bool $flat (default: false) + * @return string + */ + public function get_item_data( $cart_item, $flat = false ) { + $item_data = array(); + + // Variation values are shown only if they are not found in the title as of 3.0. + // This is because variation titles display the attributes. + if ( $cart_item['data']->is_type( 'variation' ) && is_array( $cart_item['variation'] ) ) { + foreach ( $cart_item['variation'] as $name => $value ) { + $taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) ); + + // If this is a term slug, get the term's nice name + if ( taxonomy_exists( $taxonomy ) ) { + $term = get_term_by( 'slug', $value, $taxonomy ); + if ( ! is_wp_error( $term ) && $term && $term->name ) { + $value = $term->name; + } + $label = wc_attribute_label( $taxonomy ); + + // If this is a custom option slug, get the options name. + } else { + $value = apply_filters( 'woocommerce_variation_option_name', $value ); + $label = wc_attribute_label( str_replace( 'attribute_', '', $name ), $cart_item['data'] ); + } + + // Check the nicename against the title. + if ( '' === $value || wc_is_attribute_in_product_name( $value, $cart_item['data']->get_name() ) ) { + continue; + } + + $item_data[] = array( + 'key' => $label, + 'value' => $value, + ); + } + } + + // Filter item data to allow 3rd parties to add more to the array + $item_data = apply_filters( 'woocommerce_get_item_data', $item_data, $cart_item ); + + // Format item data ready to display + foreach ( $item_data as $key => $data ) { + // Set hidden to true to not display meta on cart. + if ( ! empty( $data['hidden'] ) ) { + unset( $item_data[ $key ] ); + continue; + } + $item_data[ $key ]['key'] = ! empty( $data['key'] ) ? $data['key'] : $data['name']; + $item_data[ $key ]['display'] = ! empty( $data['display'] ) ? $data['display'] : $data['value']; + } + + // Output flat or in list format + if ( sizeof( $item_data ) > 0 ) { + ob_start(); + + if ( $flat ) { + foreach ( $item_data as $data ) { + echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['display'] ) . "\n"; + } + } else { + wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) ); + } + + return ob_get_clean(); + } + + return ''; + } + + /** + * Gets cross sells based on the items in the cart. + * + * @return array cross_sells (item ids) + */ + public function get_cross_sells() { + $cross_sells = array(); + $in_cart = array(); + if ( ! $this->is_empty() ) { foreach ( $this->get_cart() as $cart_item_key => $values ) { - if ( $values['variation_id'] > 0 && $values['data']->variation_has_stock ) { - // Variation has stock levels defined so its handled individually - $quantities[ $values['variation_id'] ] = isset( $quantities[ $values['variation_id'] ] ) ? $quantities[ $values['variation_id'] ] + $values['quantity'] : $values['quantity']; - } else { - $quantities[ $values['product_id'] ] = isset( $quantities[ $values['product_id'] ] ) ? $quantities[ $values['product_id'] ] + $values['quantity'] : $values['quantity']; + if ( $values['quantity'] > 0 ) { + $cross_sells = array_merge( $values['data']->get_cross_sell_ids(), $cross_sells ); + $in_cart[] = $values['product_id']; } } + } + $cross_sells = array_diff( $cross_sells, $in_cart ); + return wp_parse_id_list( $cross_sells ); + } - return $quantities; + /** + * Gets the url to the cart page. + * + * @deprecated 2.5.0 in favor to wc_get_cart_url() + * + * @return string url to page + */ + public function get_cart_url() { + return wc_get_cart_url(); + } + + /** + * Gets the url to the checkout page. + * + * @deprecated 2.5.0 in favor to wc_get_checkout_url() + * + * @return string url to page + */ + public function get_checkout_url() { + return wc_get_checkout_url(); + } + + /** + * Gets the url to remove an item from the cart. + * + * @param string $cart_item_key contains the id of the cart item + * @return string url to page + */ + public function get_remove_url( $cart_item_key ) { + $cart_page_url = wc_get_page_permalink( 'cart' ); + return apply_filters( 'woocommerce_get_remove_url', $cart_page_url ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, $cart_page_url ), 'woocommerce-cart' ) : '' ); + } + + /** + * Gets the url to re-add an item into the cart. + * + * @param string $cart_item_key + * @return string url to page + */ + public function get_undo_url( $cart_item_key ) { + $cart_page_url = wc_get_page_permalink( 'cart' ); + + $query_args = array( + 'undo_item' => $cart_item_key, + ); + + return apply_filters( 'woocommerce_get_undo_url', $cart_page_url ? wp_nonce_url( add_query_arg( $query_args, $cart_page_url ), 'woocommerce-cart' ) : '', $cart_item_key ); + } + + /** + * Returns the contents of the cart in an array. + * + * @return array contents of the cart + */ + public function get_cart() { + if ( ! did_action( 'wp_loaded' ) ) { + wc_doing_it_wrong( __FUNCTION__, __( 'Get cart should not be called before the wp_loaded action.', 'woocommerce' ), '2.3' ); + } + if ( ! did_action( 'woocommerce_cart_loaded_from_session' ) ) { + $this->get_cart_from_session(); + } + return array_filter( (array) $this->cart_contents ); + } + + /** + * Returns the contents of the cart in an array without the 'data' element. + * + * @return array contents of the cart + */ + public function get_cart_for_session() { + $cart_session = array(); + + if ( $this->get_cart() ) { + foreach ( $this->get_cart() as $key => $values ) { + $cart_session[ $key ] = $values; + unset( $cart_session[ $key ]['data'] ); // Unset product object + } } - /** - * Looks through cart items and checks the posts are not trashed or deleted. - * @return bool|WP_Error - */ - public function check_cart_item_validity() { - foreach ( $this->get_cart() as $cart_item_key => $values ) { + return $cart_session; + } - $_product = $values['data']; + /** + * Returns a specific item in the cart. + * + * @param string $item_key Cart item key. + * @return array Item data + */ + public function get_cart_item( $item_key ) { + if ( isset( $this->cart_contents[ $item_key ] ) ) { + return $this->cart_contents[ $item_key ]; + } - if ( ! $_product || ! $_product->exists() || $_product->post->post_status == 'trash' ) { - $this->set_quantity( $cart_item_key, 0 ); + return array(); + } - return new WP_Error( 'invalid', __( 'An item which is no longer available was removed from your cart.', 'woocommerce' ) ); + /** + * Returns the cart and shipping taxes, merged. + * + * @return array merged taxes + */ + public function get_taxes() { + $taxes = array(); + + // Merge + foreach ( array_keys( $this->taxes + $this->shipping_taxes ) as $key ) { + $taxes[ $key ] = ( isset( $this->shipping_taxes[ $key ] ) ? $this->shipping_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 ); + } + + return apply_filters( 'woocommerce_cart_get_taxes', $taxes, $this ); + } + + /** + * Get taxes, merged by code, formatted ready for output. + * + * @return array + */ + public function get_tax_totals() { + $taxes = $this->get_taxes(); + $tax_totals = array(); + + foreach ( $taxes as $key => $tax ) { + $code = WC_Tax::get_rate_code( $key ); + + if ( $code || apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) === $key ) { + if ( ! isset( $tax_totals[ $code ] ) ) { + $tax_totals[ $code ] = new stdClass(); + $tax_totals[ $code ]->amount = 0; } + $tax_totals[ $code ]->tax_rate_id = $key; + $tax_totals[ $code ]->is_compound = WC_Tax::is_compound( $key ); + $tax_totals[ $code ]->label = WC_Tax::get_rate_label( $key ); + $tax_totals[ $code ]->amount += wc_round_tax_total( $tax ); + $tax_totals[ $code ]->formatted_amount = wc_price( wc_round_tax_total( $tax_totals[ $code ]->amount ) ); } - - return true; } - /** - * Looks through the cart to check each item is in stock. If not, add an error. - * - * @access public - * @return bool|WP_Error - */ - public function check_cart_item_stock() { - global $wpdb; + if ( apply_filters( 'woocommerce_cart_hide_zero_taxes', true ) ) { + $amounts = array_filter( wp_list_pluck( $tax_totals, 'amount' ) ); + $tax_totals = array_intersect_key( $tax_totals, $amounts ); + } - $error = new WP_Error(); + return apply_filters( 'woocommerce_cart_tax_totals', $tax_totals, $this ); + } - $product_qty_in_cart = $this->get_cart_item_quantities(); + /** + * Get all tax classes for items in the cart. + * @return array + */ + public function get_cart_item_tax_classes() { + $found_tax_classes = array(); - // First stock check loop - foreach ( $this->get_cart() as $cart_item_key => $values ) { + foreach ( WC()->cart->get_cart() as $item ) { + $found_tax_classes[] = $item['data']->get_tax_class(); + } - $_product = $values['data']; + return array_unique( $found_tax_classes ); + } - /** - * Check stock based on inventory - */ - if ( $_product->managing_stock() ) { + /** + * Determines the value that the customer spent and the subtotal + * displayed, used for things like coupon validation. + * + * Since the coupon lines are displayed based on the TAX DISPLAY value + * of cart, this is used to determine the spend. + * + * If cart totals are shown including tax, use the subtotal. + * If cart totals are shown excluding tax, use the subtotal ex tax + * (tax is shown after coupons). + * + * @since 2.6.0 + * @return string + */ + public function get_displayed_subtotal() { + if ( 'incl' === $this->tax_display_cart ) { + return wc_format_decimal( $this->subtotal ); + } elseif ( 'excl' === $this->tax_display_cart ) { + return wc_format_decimal( $this->subtotal_ex_tax ); + } + } - /** - * Check the stock for this item individually - */ - if ( ! $_product->is_in_stock() || ! $_product->has_enough_stock( $values['quantity'] ) ) { - $error->add( 'out-of-stock', sprintf(__( 'Sorry, we do not have enough "%s" in stock to fulfill your order (%s in stock). Please edit your cart and try again. We apologise for any inconvenience caused.', 'woocommerce' ), $_product->get_title(), $_product->stock ) ); - return $error; - } + /* + * Add to cart handling. + */ - // For later on... - $key = '_product_id'; - $value = $values['product_id']; - $in_cart = $values['quantity']; + /** + * Check if product is in the cart and return cart item key. + * + * Cart item key will be unique based on the item and its properties, such as variations. + * + * @param mixed id of product to find in the cart + * @return string cart item key + */ + public function find_product_in_cart( $cart_id = false ) { + if ( false !== $cart_id ) { + if ( is_array( $this->cart_contents ) && isset( $this->cart_contents[ $cart_id ] ) ) { + return $cart_id; + } + } + return ''; + } - /** - * Next check entire cart quantities - */ - if ( $values['variation_id'] && $_product->variation_has_stock && isset( $product_qty_in_cart[ $values['variation_id'] ] ) ) { + /** + * Generate a unique ID for the cart item being added. + * + * @param int $product_id - id of the product the key is being generated for + * @param int $variation_id of the product the key is being generated for + * @param array $variation data for the cart item + * @param array $cart_item_data other cart item data passed which affects this items uniqueness in the cart + * @return string cart item key + */ + public function generate_cart_id( $product_id, $variation_id = 0, $variation = array(), $cart_item_data = array() ) { + $id_parts = array( $product_id ); - $key = '_variation_id'; - $value = $values['variation_id']; - $in_cart = $product_qty_in_cart[ $values['variation_id'] ]; + if ( $variation_id && 0 != $variation_id ) { + $id_parts[] = $variation_id; + } - if ( ! $_product->has_enough_stock( $product_qty_in_cart[ $values['variation_id'] ] ) ) { - $error->add( 'out-of-stock', sprintf(__( 'Sorry, we do not have enough "%s" in stock to fulfill your order (%s in stock). Please edit your cart and try again. We apologise for any inconvenience caused.', 'woocommerce' ), $_product->get_title(), $_product->stock ) ); - return $error; - } + if ( is_array( $variation ) && ! empty( $variation ) ) { + $variation_key = ''; + foreach ( $variation as $key => $value ) { + $variation_key .= trim( $key ) . trim( $value ); + } + $id_parts[] = $variation_key; + } - } elseif ( isset( $product_qty_in_cart[ $values['product_id'] ] ) ) { - - $in_cart = $product_qty_in_cart[ $values['product_id'] ]; - - if ( ! $_product->has_enough_stock( $product_qty_in_cart[ $values['product_id'] ] ) ) { - $error->add( 'out-of-stock', sprintf(__( 'Sorry, we do not have enough "%s" in stock to fulfill your order (%s in stock). Please edit your cart and try again. We apologise for any inconvenience caused.', 'woocommerce' ), $_product->get_title(), $_product->stock ) ); - return $error; - } - - } - - /** - * Finally consider any held stock, from pending orders - */ - if ( get_option( 'woocommerce_hold_stock_minutes' ) > 0 && ! $_product->backorders_allowed() ) { - - $order_id = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0; - - $held_stock = $wpdb->get_var( $wpdb->prepare( " - SELECT SUM( order_item_meta.meta_value ) AS held_qty - - FROM {$wpdb->posts} AS posts - - 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 posts.post_type = 'shop_order' - AND posts.post_status = 'wc-pending' - AND posts.ID != %d - ", $key, $value, $order_id ) ); - - if ( $_product->stock < ( $held_stock + $in_cart ) ) { - $error->add( 'out-of-stock', sprintf(__( 'Sorry, we do not have enough "%s" in stock to fulfill your order right now. Please try again in %d minutes or edit your cart and try again. We apologise for any inconvenience caused.', 'woocommerce' ), $_product->get_title(), get_option( 'woocommerce_hold_stock_minutes' ) ) ); - return $error; - } - } - - /** - * Check stock based on stock-status - */ - } else { - if ( ! $_product->is_in_stock() ) { - $error->add( 'out-of-stock', sprintf(__( 'Sorry, "%s" is not in stock. Please edit your cart and try again. We apologise for any inconvenience caused.', 'woocommerce' ), $_product->get_title() ) ); - return $error; - } + if ( is_array( $cart_item_data ) && ! empty( $cart_item_data ) ) { + $cart_item_data_key = ''; + foreach ( $cart_item_data as $key => $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + $value = http_build_query( $value ); } + $cart_item_data_key .= trim( $key ) . trim( $value ); + + } + $id_parts[] = $cart_item_data_key; + } + + return apply_filters( 'woocommerce_cart_id', md5( implode( '_', $id_parts ) ), $product_id, $variation_id, $variation, $cart_item_data ); + } + + /** + * Add a product to the cart. + * + * @param int $product_id contains the id of the product to add to the cart + * @param int $quantity contains the quantity of the item to add + * @param int $variation_id + * @param array $variation attribute values + * @param array $cart_item_data extra cart item data we want to pass into the item + * @return string|bool $cart_item_key + */ + public function add_to_cart( $product_id = 0, $quantity = 1, $variation_id = 0, $variation = array(), $cart_item_data = array() ) { + // Wrap in try catch so plugins can throw an exception to prevent adding to cart + try { + $product_id = absint( $product_id ); + $variation_id = absint( $variation_id ); + + // Ensure we don't add a variation to the cart directly by variation ID + if ( 'product_variation' === get_post_type( $product_id ) ) { + $variation_id = $product_id; + $product_id = wp_get_post_parent_id( $variation_id ); } - return true; - } + // Get the product + $product_data = wc_get_product( $variation_id ? $variation_id : $product_id ); - /** - * Gets and formats a list of cart item data + variations for display on the frontend. - * - * @access public - * @param array $cart_item - * @param bool $flat (default: false) - * @return string - */ - public function get_item_data( $cart_item, $flat = false ) { - $item_data = array(); + // Filter quantity being added to the cart before stock checks + $quantity = apply_filters( 'woocommerce_add_to_cart_quantity', $quantity, $product_id ); - // Variation data - if ( ! empty( $cart_item['data']->variation_id ) && is_array( $cart_item['variation'] ) ) { - - $variation_list = array(); - - foreach ( $cart_item['variation'] as $name => $value ) { - - if ( '' === $value ) - continue; - - $taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) ); - - // If this is a term slug, get the term's nice name - if ( taxonomy_exists( $taxonomy ) ) { - $term = get_term_by( 'slug', $value, $taxonomy ); - if ( ! is_wp_error( $term ) && $term && $term->name ) { - $value = $term->name; - } - $label = wc_attribute_label( $taxonomy ); - - // If this is a custom option slug, get the options name - } else { - $value = apply_filters( 'woocommerce_variation_option_name', $value ); - $product_attributes = $cart_item['data']->get_attributes(); - if ( isset( $product_attributes[ str_replace( 'attribute_', '', $name ) ] ) ) { - $label = wc_attribute_label( $product_attributes[ str_replace( 'attribute_', '', $name ) ]['name'] ); - } else { - $label = $name; - } - } - - $item_data[] = array( - 'key' => $label, - 'value' => $value - ); - } - } - - // Other data - returned as array with name/value values - $other_data = apply_filters( 'woocommerce_get_item_data', array(), $cart_item ); - - if ( $other_data && is_array( $other_data ) && sizeof( $other_data ) > 0 ) { - - foreach ( $other_data as $data ) { - // Set hidden to true to not display meta on cart. - if ( empty( $data['hidden'] ) ) { - $display_value = ! empty( $data['display'] ) ? $data['display'] : $data['value']; - - $item_data[] = array( - 'key' => $data['name'], - 'value' => $display_value - ); - } - } - } - - // Output flat or in list format - if ( sizeof( $item_data ) > 0 ) { - - ob_start(); - - if ( $flat ) { - foreach ( $item_data as $data ) { - echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['value'] ) . "\n"; - } - } else { - wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) ); - } - - return ob_get_clean(); - } - - return ''; - } - - /** - * Gets cross sells based on the items in the cart. - * - * @return array cross_sells (item ids) - */ - public function get_cross_sells() { - $cross_sells = array(); - $in_cart = array(); - if ( sizeof( $this->get_cart() ) > 0 ) { - foreach ( $this->get_cart() as $cart_item_key => $values ) { - if ( $values['quantity'] > 0 ) { - $cross_sells = array_merge( $values['data']->get_cross_sells(), $cross_sells ); - $in_cart[] = $values['product_id']; - } - } - } - $cross_sells = array_diff( $cross_sells, $in_cart ); - return $cross_sells; - } - - /** - * Gets the url to the cart page. - * - * @return string url to page - */ - public function get_cart_url() { - $cart_page_id = wc_get_page_id( 'cart' ); - return apply_filters( 'woocommerce_get_cart_url', $cart_page_id ? get_permalink( $cart_page_id ) : '' ); - } - - /** - * Gets the url to the checkout page. - * - * @return string url to page - */ - public function get_checkout_url() { - $checkout_page_id = wc_get_page_id( 'checkout' ); - $checkout_url = ''; - if ( $checkout_page_id ) { - if ( is_ssl() || get_option('woocommerce_force_ssl_checkout') == 'yes' ) { - $checkout_url = str_replace( 'http:', 'https:', get_permalink( $checkout_page_id ) ); - } else { - $checkout_url = get_permalink( $checkout_page_id ); - } - } - return apply_filters( 'woocommerce_get_checkout_url', $checkout_url ); - } - - /** - * Gets the url to remove an item from the cart. - * - * @param string cart_item_key contains the id of the cart item - * @return string url to page - */ - public function get_remove_url( $cart_item_key ) { - $cart_page_id = wc_get_page_id('cart'); - return apply_filters( 'woocommerce_get_remove_url', $cart_page_id ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, get_permalink( $cart_page_id ) ), 'woocommerce-cart' ) : '' ); - } - - /** - * Returns the contents of the cart in an array. - * - * @return array contents of the cart - */ - public function get_cart() { - return array_filter( (array) $this->cart_contents ); - } - - /** - * Returns the contents of the cart in an array without the 'data' element. - * - * @return array contents of the cart - */ - private function get_cart_for_session() { - - $cart_session = array(); - - if ( $this->get_cart() ) { - foreach ( $this->get_cart() as $key => $values ) { - $cart_session[ $key ] = $values; - unset( $cart_session[ $key ]['data'] ); // Unset product object - } - } - - return $cart_session; - } - - /** - * Returns the cart and shipping taxes, merged. - * - * @return array merged taxes - */ - public function get_taxes() { - $taxes = array(); - - // Merge - foreach ( array_keys( $this->taxes + $this->shipping_taxes ) as $key ) { - $taxes[ $key ] = ( isset( $this->shipping_taxes[ $key ] ) ? $this->shipping_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 ); - } - - return apply_filters( 'woocommerce_cart_get_taxes', $taxes, $this ); - } - - /** - * Get taxes, merged by code, formatted ready for output. - * - * @access public - * @return array - */ - public function get_tax_totals() { - $taxes = $this->get_taxes(); - $tax_totals = array(); - - foreach ( $taxes as $key => $tax ) { - - $code = $this->tax->get_rate_code( $key ); - - if ( $code ) { - if ( ! isset( $tax_totals[ $code ] ) ) { - $tax_totals[ $code ] = new stdClass(); - $tax_totals[ $code ]->amount = 0; - } - - $tax_totals[ $code ]->tax_rate_id = $key; - $tax_totals[ $code ]->is_compound = $this->tax->is_compound( $key ); - $tax_totals[ $code ]->label = $this->tax->get_rate_label( $key ); - $tax_totals[ $code ]->amount += wc_round_tax_total( $tax ); - $tax_totals[ $code ]->formatted_amount = wc_price( wc_round_tax_total( $tax_totals[ $code ]->amount ) ); - } - } - - return apply_filters( 'woocommerce_cart_tax_totals', $tax_totals, $this ); - } - - /*-----------------------------------------------------------------------------------*/ - /* Add to cart handling */ - /*-----------------------------------------------------------------------------------*/ - - /** - * Check if product is in the cart and return cart item key. - * - * Cart item key will be unique based on the item and its properties, such as variations. - * - * @param mixed id of product to find in the cart - * @return string cart item key - */ - public function find_product_in_cart( $cart_id = false ) { - if ( $cart_id !== false ) { - if ( is_array( $this->cart_contents ) ) { - foreach ( $this->cart_contents as $cart_item_key => $cart_item ) { - if ( $cart_item_key == $cart_id ) { - return $cart_item_key; - } - } - } - } - return ''; - } - - /** - * Generate a unique ID for the cart item being added. - * - * @param int $product_id - id of the product the key is being generated for - * @param int $variation_id of the product the key is being generated for - * @param array $variation data for the cart item - * @param array $cart_item_data other cart item data passed which affects this items uniqueness in the cart - * @return string cart item key - */ - public function generate_cart_id( $product_id, $variation_id = 0, $variation = array(), $cart_item_data = array() ) { - $id_parts = array( $product_id ); - - if ( $variation_id && 0 != $variation_id ) - $id_parts[] = $variation_id; - - if ( is_array( $variation ) && ! empty( $variation ) ) { - $variation_key = ''; - foreach ( $variation as $key => $value ) { - $variation_key .= trim( $key ) . trim( $value ); - } - $id_parts[] = $variation_key; - } - - if ( is_array( $cart_item_data ) && ! empty( $cart_item_data ) ) { - $cart_item_data_key = ''; - foreach ( $cart_item_data as $key => $value ) { - if ( is_array( $value ) ) $value = http_build_query( $value ); - $cart_item_data_key .= trim($key) . trim($value); - } - $id_parts[] = $cart_item_data_key; - } - - return md5( implode( '_', $id_parts ) ); - } - - /** - * Add a product to the cart. - * - * @param string $product_id contains the id of the product to add to the cart - * @param string $quantity contains the quantity of the item to add - * @param int $variation_id - * @param array $variation attribute values - * @param array $cart_item_data extra cart item data we want to pass into the item - * @return string $cart_item_key - */ - public function add_to_cart( $product_id, $quantity = 1, $variation_id = '', $variation = '', $cart_item_data = array() ) { - - if ( $quantity <= 0 ) { + // Sanity check + if ( $quantity <= 0 || ! $product_data || 'trash' === $product_data->get_status() ) { return false; } // Load cart item data - may be added by other plugins $cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id ); - + // Generate a ID based on product ID, variation ID, variation data, and other cart item data $cart_id = $this->generate_cart_id( $product_id, $variation_id, $variation, $cart_item_data ); - - // See if this product and its options is already in the cart + + // Find the cart item key in the existing cart $cart_item_key = $this->find_product_in_cart( $cart_id ); - // Ensure we don't add a variation to the cart directly by variation ID - if ( 'product_variation' == get_post_type( $product_id ) ) { - $variation_id = $product_id; - $product_id = wp_get_post_parent_id( $variation_id ); + // Force quantity to 1 if sold individually and check for existing item in cart + if ( $product_data->is_sold_individually() ) { + $quantity = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $quantity, $product_id, $variation_id, $cart_item_data ); + $found_in_cart = apply_filters( 'woocommerce_add_to_cart_sold_individually_found_in_cart', $cart_item_key && $this->cart_contents[ $cart_item_key ]['quantity'] > 0, $product_id, $variation_id, $cart_item_data, $cart_id ); + + if ( $found_in_cart ) { + /* translators: %s: product name */ + throw new Exception( sprintf( '%s %s', wc_get_cart_url(), __( 'View cart', 'woocommerce' ), sprintf( __( 'You cannot add another "%s" to your cart.', 'woocommerce' ), $product_data->get_name() ) ) ); + } } - - // Get the product - $product_data = get_product( $variation_id ? $variation_id : $product_id ); - - if ( ! $product_data ) - return false; - - // Force quantity to 1 if sold individually - if ( $product_data->is_sold_individually() ) - $quantity = 1; // Check product is_purchasable if ( ! $product_data->is_purchasable() ) { - wc_add_notice( __( 'Sorry, this product cannot be purchased.', 'woocommerce' ), 'error' ); - return false; + throw new Exception( __( 'Sorry, this product cannot be purchased.', 'woocommerce' ) ); } // Stock check - only check if we're managing stock and backorders are not allowed if ( ! $product_data->is_in_stock() ) { - - wc_add_notice( sprintf( __( 'You cannot add "%s" to the cart because the product is out of stock.', 'woocommerce' ), $product_data->get_title() ), 'error' ); - - return false; - } elseif ( ! $product_data->has_enough_stock( $quantity ) ) { - - wc_add_notice( sprintf(__( 'You cannot add that amount of "%s" to the cart because there is not enough stock (%s remaining).', 'woocommerce' ), $product_data->get_title(), $product_data->get_stock_quantity() ), 'error' ); - - return false; + throw new Exception( sprintf( __( 'You cannot add "%s" to the cart because the product is out of stock.', 'woocommerce' ), $product_data->get_name() ) ); } - // Downloadable/virtual qty check - if ( $product_data->is_sold_individually() ) { - $in_cart_quantity = $cart_item_key ? $this->cart_contents[$cart_item_key]['quantity'] : 0; - - // If it's greater than 0, it's already in the cart - if ( $in_cart_quantity > 0 ) { - wc_add_notice( sprintf( - '%s %s', - $this->get_cart_url(), - __( 'View Cart', 'woocommerce' ), - sprintf( __( 'You cannot add another "%s" to your cart.', 'woocommerce' ), $product_data->get_title() ) - ), 'error' ); - return false; - } + if ( ! $product_data->has_enough_stock( $quantity ) ) { + /* translators: 1: product name 2: quantity in stock */ + throw new Exception( sprintf( __( 'You cannot add that amount of "%1$s" to the cart because there is not enough stock (%2$s remaining).', 'woocommerce' ), $product_data->get_name(), wc_format_stock_quantity_for_display( $product_data->get_stock_quantity(), $product_data ) ) ); } // Stock check - this time accounting for whats already in-cart - $product_qty_in_cart = $this->get_cart_item_quantities(); - if ( $product_data->managing_stock() ) { + $products_qty_in_cart = $this->get_cart_item_quantities(); - // Variations - if ( $variation_id && $product_data->variation_has_stock ) { - - if ( isset( $product_qty_in_cart[ $variation_id ] ) && ! $product_data->has_enough_stock( $product_qty_in_cart[ $variation_id ] + $quantity ) ) { - wc_add_notice( sprintf( - '%s %s', - $this->get_cart_url(), - __( 'View Cart', 'woocommerce' ), - sprintf( __( 'You cannot add that amount to the cart — we have %s in stock and you already have %s in your cart.', 'woocommerce' ), $product_data->get_stock_quantity(), $product_qty_in_cart[ $variation_id ] ) - ), 'error' ); - return false; - } - - // Products - } else { - - if ( isset( $product_qty_in_cart[ $product_id ] ) && ! $product_data->has_enough_stock( $product_qty_in_cart[ $product_id ] + $quantity ) ) { - wc_add_notice( sprintf( - '%s %s', - $this->get_cart_url(), - __( 'View Cart', 'woocommerce' ), - sprintf( __( 'You cannot add that amount to the cart — we have %s in stock and you already have %s in your cart.', 'woocommerce' ), $product_data->get_stock_quantity(), $product_qty_in_cart[ $product_id ] ) - ), 'error' ); - return false; - } - + if ( isset( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] ) && ! $product_data->has_enough_stock( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] + $quantity ) ) { + throw new Exception( sprintf( + '%s %s', + wc_get_cart_url(), + __( 'View Cart', 'woocommerce' ), + sprintf( __( 'You cannot add that amount to the cart — we have %1$s in stock and you already have %2$s in your cart.', 'woocommerce' ), wc_format_stock_quantity_for_display( $product_data->get_stock_quantity(), $product_data ), wc_format_stock_quantity_for_display( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ], $product_data ) ) + ) ); } - } // If cart_item_key is set, the item is already in the cart if ( $cart_item_key ) { - - $new_quantity = $quantity + $this->cart_contents[$cart_item_key]['quantity']; - + $new_quantity = $quantity + $this->cart_contents[ $cart_item_key ]['quantity']; $this->set_quantity( $cart_item_key, $new_quantity, false ); - } else { - $cart_item_key = $cart_id; // Add item after merging with $cart_item_data - hook to allow plugins to modify cart item - $this->cart_contents[$cart_item_key] = apply_filters( 'woocommerce_add_cart_item', array_merge( $cart_item_data, array( + $this->cart_contents[ $cart_item_key ] = apply_filters( 'woocommerce_add_cart_item', array_merge( $cart_item_data, array( 'product_id' => $product_id, 'variation_id' => $variation_id, 'variation' => $variation, 'quantity' => $quantity, - 'data' => $product_data + 'data' => $product_data, ) ), $cart_item_key ); - } if ( did_action( 'wp' ) ) { - $this->set_cart_cookies( sizeof( $this->cart_contents ) > 0 ); - } + $this->set_cart_cookies( ! $this->is_empty() ); + } do_action( 'woocommerce_add_to_cart', $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ); - $this->calculate_totals(); - return $cart_item_key; - } - /** - * Set the quantity for an item in the cart. - * - * @param string cart_item_key contains the id of the cart item - * @param string quantity contains the quantity of the item - * @param boolean $refresh_totals whether or not to calculate totals after setting the new qty - */ - public function set_quantity( $cart_item_key, $quantity = 1, $refresh_totals = true ) { - if ( $quantity == 0 || $quantity < 0 ) { - do_action( 'woocommerce_before_cart_item_quantity_zero', $cart_item_key ); - unset( $this->cart_contents[ $cart_item_key ] ); - } else { - $this->cart_contents[ $cart_item_key ]['quantity'] = $quantity; - do_action( 'woocommerce_after_cart_item_quantity_update', $cart_item_key, $quantity ); - } - - if ( $refresh_totals ) { - $this->calculate_totals(); - } - } - - /*-----------------------------------------------------------------------------------*/ - /* Cart Calculation Functions */ - /*-----------------------------------------------------------------------------------*/ - - /** - * Reset cart totals and clear sessions. - * - * @access private - * @return void - */ - private function reset() { - foreach ( $this->cart_session_data as $key => $default ) { - $this->$key = $default; - unset( WC()->session->$key ); - } - } - - /** - * Calculate totals for the items in the cart. - * - * @access public - */ - public function calculate_totals() { - - $this->reset(); - - do_action( 'woocommerce_before_calculate_totals', $this ); - - if ( sizeof( $this->get_cart() ) == 0 ) { - $this->set_session(); - return; - } - - $tax_rates = array(); - $shop_tax_rates = array(); - - /** - * Calculate subtotals for items. This is done first so that discount logic can use the values. - */ - foreach ( $this->get_cart() as $cart_item_key => $values ) { - - $_product = $values['data']; - - // Count items + weight - $this->cart_contents_weight += $_product->get_weight() * $values['quantity']; - $this->cart_contents_count += $values['quantity']; - - // Prices - $base_price = $_product->get_price(); - $line_price = $_product->get_price() * $values['quantity']; - - $line_subtotal = 0; - $line_subtotal_tax = 0; - - /** - * No tax to calculate - */ - if ( ! $_product->is_taxable() ) { - - // Subtotal is the undiscounted price - $this->subtotal += $line_price; - $this->subtotal_ex_tax += $line_price; - - /** - * Prices include tax - * - * To prevent rounding issues we need to work with the inclusive price where possible - * otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would - * be 8.325 leading to totals being 1p off - * - * Pre tax coupons come off the price the customer thinks they are paying - tax is calculated - * afterwards. - * - * e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that - */ - } elseif ( $this->prices_include_tax ) { - - // Get base tax rates - if ( empty( $shop_tax_rates[ $_product->tax_class ] ) ) - $shop_tax_rates[ $_product->tax_class ] = $this->tax->get_shop_base_rate( $_product->tax_class ); - - // Get item tax rates - if ( empty( $tax_rates[ $_product->get_tax_class() ] ) ) - $tax_rates[ $_product->get_tax_class() ] = $this->tax->get_rates( $_product->get_tax_class() ); - - $base_tax_rates = $shop_tax_rates[ $_product->tax_class ]; - $item_tax_rates = $tax_rates[ $_product->get_tax_class() ]; - - /** - * ADJUST TAX - Calculations when base tax is not equal to the item tax - */ - if ( $item_tax_rates !== $base_tax_rates ) { - - // Work out a new base price without the shop's base tax - $taxes = $this->tax->calc_tax( $line_price, $base_tax_rates, true, true ); - - // Now we have a new item price (excluding TAX) - $line_subtotal = $line_price - array_sum( $taxes ); - - // Now add modifed taxes - $tax_result = $this->tax->calc_tax( $line_subtotal, $item_tax_rates ); - $line_subtotal_tax = array_sum( $tax_result ); - - /** - * Regular tax calculation (customer inside base and the tax class is unmodified - */ - } else { - - // Calc tax normally - $taxes = $this->tax->calc_tax( $line_price, $item_tax_rates, true ); - $line_subtotal_tax = array_sum( $taxes ); - $line_subtotal = $line_price - array_sum( $taxes ); - } - - /** - * Prices exclude tax - * - * This calculation is simpler - work with the base, untaxed price. - */ - } else { - - // Get item tax rates - if ( empty( $tax_rates[ $_product->get_tax_class() ] ) ) - $tax_rates[ $_product->get_tax_class() ] = $this->tax->get_rates( $_product->get_tax_class() ); - - $item_tax_rates = $tax_rates[ $_product->get_tax_class() ]; - - // Base tax for line before discount - we will store this in the order data - $taxes = $this->tax->calc_tax( $line_price, $item_tax_rates ); - $line_subtotal_tax = array_sum( $taxes ); - $line_subtotal = $line_price; - } - - // Add to main subtotal - $this->subtotal += $line_subtotal + $line_subtotal_tax; - $this->subtotal_ex_tax += $line_subtotal; - } - - /** - * Calculate totals for items - */ - foreach ( $this->get_cart() as $cart_item_key => $values ) { - - $_product = $values['data']; - - // Prices - $base_price = $_product->get_price(); - $line_price = $_product->get_price() * $values['quantity']; - - /** - * No tax to calculate - */ - if ( ! $_product->is_taxable() ) { - - // Discounted Price (price with any pre-tax discounts applied) - $discounted_price = $this->get_discounted_price( $values, $base_price, true ); - $discounted_tax_amount = 0; - $tax_amount = 0; - $line_subtotal_tax = 0; - $line_subtotal = $line_price; - $line_tax = 0; - $line_total = $this->tax->round( $discounted_price * $values['quantity'] ); - - /** - * Prices include tax - */ - } elseif ( $this->prices_include_tax ) { - - $base_tax_rates = $shop_tax_rates[ $_product->tax_class ]; - $item_tax_rates = $tax_rates[ $_product->get_tax_class() ]; - - /** - * ADJUST TAX - Calculations when base tax is not equal to the item tax - */ - if ( $item_tax_rates !== $base_tax_rates ) { - - // Work out a new base price without the shop's base tax - $taxes = $this->tax->calc_tax( $line_price, $base_tax_rates, true, true ); - - // Now we have a new item price (excluding TAX) - $line_subtotal = round( $line_price - array_sum( $taxes ), WC_ROUNDING_PRECISION ); - - // Now add modifed taxes - $taxes = $this->tax->calc_tax( $line_subtotal, $item_tax_rates ); - $line_subtotal_tax = array_sum( $taxes ); - - // Adjusted price (this is the price including the new tax rate) - $adjusted_price = ( $line_subtotal + $line_subtotal_tax ) / $values['quantity']; - - // Apply discounts - $discounted_price = $this->get_discounted_price( $values, $adjusted_price, true ); - $discounted_taxes = $this->tax->calc_tax( $discounted_price * $values['quantity'], $item_tax_rates, true ); - $line_tax = array_sum( $discounted_taxes ); - $line_total = ( $discounted_price * $values['quantity'] ) - $line_tax; - - /** - * Regular tax calculation (customer inside base and the tax class is unmodified - */ - } else { - - // Work out a new base price without the shop's base tax - $taxes = $this->tax->calc_tax( $line_price, $item_tax_rates, true ); - - // Now we have a new item price (excluding TAX) - $line_subtotal = $line_price - array_sum( $taxes ); - $line_subtotal_tax = array_sum( $taxes ); - - // Calc prices and tax (discounted) - $discounted_price = $this->get_discounted_price( $values, $base_price, true ); - $discounted_taxes = $this->tax->calc_tax( $discounted_price * $values['quantity'], $item_tax_rates, true ); - $line_tax = array_sum( $discounted_taxes ); - $line_total = ( $discounted_price * $values['quantity'] ) - $line_tax; - } - - // Tax rows - merge the totals we just got - foreach ( array_keys( $this->taxes + $discounted_taxes ) as $key ) { - $this->taxes[ $key ] = ( isset( $discounted_taxes[ $key ] ) ? $discounted_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 ); - } - - /** - * Prices exclude tax - */ - } else { - - $item_tax_rates = $tax_rates[ $_product->get_tax_class() ]; - - // Work out a new base price without the shop's base tax - $taxes = $this->tax->calc_tax( $line_price, $item_tax_rates ); - - // Now we have the item price (excluding TAX) - $line_subtotal = $line_price; - $line_subtotal_tax = array_sum( $taxes ); - - // Now calc product rates - $discounted_price = $this->get_discounted_price( $values, $base_price, true ); - $discounted_taxes = $this->tax->calc_tax( $discounted_price * $values['quantity'], $item_tax_rates ); - $discounted_tax_amount = array_sum( $discounted_taxes ); - $line_tax = $discounted_tax_amount; - $line_total = $discounted_price * $values['quantity']; - - // Tax rows - merge the totals we just got - foreach ( array_keys( $this->taxes + $discounted_taxes ) as $key ) { - $this->taxes[ $key ] = ( isset( $discounted_taxes[ $key ] ) ? $discounted_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 ); - } - } - - // Add any product discounts (after tax) - $this->apply_product_discounts_after_tax( $values, $line_total + $line_tax ); - - // Cart contents total is based on discounted prices and is used for the final total calculation - $this->cart_contents_total += $line_total; - - // Store costs + taxes for lines - $this->cart_contents[ $cart_item_key ]['line_total'] = $line_total; - $this->cart_contents[ $cart_item_key ]['line_tax'] = $line_tax; - $this->cart_contents[ $cart_item_key ]['line_subtotal'] = $line_subtotal; - $this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $line_subtotal_tax; - } - - // Only calculate the grand total + shipping if on the cart/checkout - if ( is_checkout() || is_cart() || defined('WOOCOMMERCE_CHECKOUT') || defined('WOOCOMMERCE_CART') ) { - - // Calculate the Shipping - $this->calculate_shipping(); - - // Trigger the fees API where developers can add fees to the cart - $this->calculate_fees(); - - // Total up/round taxes and shipping taxes - if ( $this->round_at_subtotal ) { - $this->tax_total = $this->tax->get_tax_total( $this->taxes ); - $this->shipping_tax_total = $this->tax->get_tax_total( $this->shipping_taxes ); - $this->taxes = array_map( array( $this->tax, 'round' ), $this->taxes ); - $this->shipping_taxes = array_map( array( $this->tax, 'round' ), $this->shipping_taxes ); - } else { - $this->tax_total = array_sum( $this->taxes ); - $this->shipping_tax_total = array_sum( $this->shipping_taxes ); - } - - // VAT exemption done at this point - so all totals are correct before exemption - if ( WC()->customer->is_vat_exempt() ) { - $this->remove_taxes(); - } - - // Cart Discounts (after tax) - $this->apply_cart_discounts_after_tax(); - - // Allow plugins to hook and alter totals before final total is calculated - do_action( 'woocommerce_calculate_totals', $this ); - - // Grand Total - Discounted product prices, discounted tax, shipping cost + tax, and any discounts to be added after tax (e.g. store credit) - $this->total = max( 0, apply_filters( 'woocommerce_calculated_total', round( $this->cart_contents_total + $this->tax_total + $this->shipping_tax_total + $this->shipping_total - $this->discount_total + $this->fee_total, $this->dp ), $this ) ); - - } else { - - // Set tax total to sum of all tax rows - $this->tax_total = $this->tax->get_tax_total( $this->taxes ); - - // VAT exemption done at this point - so all totals are correct before exemption - if ( WC()->customer->is_vat_exempt() ) { - $this->remove_taxes(); - } - - // Cart Discounts (after tax) - $this->apply_cart_discounts_after_tax(); - } - - $this->set_session(); - } - - /** - * remove_taxes function. - * - * @access public - * @return void - */ - public function remove_taxes() { - $this->shipping_tax_total = $this->tax_total = 0; - $this->subtotal = $this->subtotal_ex_tax; - - foreach ( $this->cart_contents as $cart_item_key => $item ) { - $this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $this->cart_contents[ $cart_item_key ]['line_tax'] = 0; - } - - // If true, zero rate is applied so '0' tax is displayed on the frontend rather than nothing. - if ( apply_filters( 'woocommerce_cart_remove_taxes_apply_zero_rate', true ) ) { - $this->taxes = $this->shipping_taxes = array( apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) => 0 ); - } else { - $this->taxes = $this->shipping_taxes = array(); - } - } - - /** - * looks at the totals to see if payment is actually required. - * - * @return bool - */ - public function needs_payment() { - return apply_filters( 'woocommerce_cart_needs_payment', $this->total > 0, $this ); - } - - /*-----------------------------------------------------------------------------------*/ - /* Shipping related functions */ - /*-----------------------------------------------------------------------------------*/ - - /** - * Uses the shipping class to calculate shipping then gets the totals when its finished. - * - * @access public - * @return void - */ - public function calculate_shipping() { - if ( $this->needs_shipping() && $this->show_shipping() ) { - WC()->shipping->calculate_shipping( $this->get_shipping_packages() ); - } else { - WC()->shipping->reset_shipping(); - } - - // Get totals for the chosen shipping method - $this->shipping_total = WC()->shipping->shipping_total; // Shipping Total - $this->shipping_taxes = WC()->shipping->shipping_taxes; // Shipping Taxes - } - - /** - * Get packages to calculate shipping for. - * - * This lets us calculate costs for carts that are shipped to multiple locations. - * - * Shipping methods are responsible for looping through these packages. - * - * By default we pass the cart itself as a package - plugins can change this - * through the filter and break it up. - * - * @since 1.5.4 - * @access public - * @return array of cart items - */ - public function get_shipping_packages() { - // Packages array for storing 'carts' - $packages = array(); - - $packages[0]['contents'] = $this->get_cart(); // Items in the package - $packages[0]['contents_cost'] = 0; // Cost of items in the package, set below - $packages[0]['applied_coupons'] = $this->applied_coupons; - $packages[0]['user']['ID'] = get_current_user_id(); - $packages[0]['destination']['country'] = WC()->customer->get_shipping_country(); - $packages[0]['destination']['state'] = WC()->customer->get_shipping_state(); - $packages[0]['destination']['postcode'] = WC()->customer->get_shipping_postcode(); - $packages[0]['destination']['city'] = WC()->customer->get_shipping_city(); - $packages[0]['destination']['address'] = WC()->customer->get_shipping_address(); - $packages[0]['destination']['address_2'] = WC()->customer->get_shipping_address_2(); - - foreach ( $this->get_cart() as $item ) - if ( $item['data']->needs_shipping() ) - if ( isset( $item['line_total'] ) ) - $packages[0]['contents_cost'] += $item['line_total']; - - return apply_filters( 'woocommerce_cart_shipping_packages', $packages ); - } - - /** - * Looks through the cart to see if shipping is actually required. - * - * @return bool whether or not the cart needs shipping - */ - public function needs_shipping() { - if ( get_option('woocommerce_calc_shipping') == 'no' ) - return false; - - $needs_shipping = false; - - if ( $this->cart_contents ) { - foreach ( $this->cart_contents as $cart_item_key => $values ) { - $_product = $values['data']; - if ( $_product->needs_shipping() ) { - $needs_shipping = true; - } - } - } - - return apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping ); - } - - /** - * Should the shipping address form be shown - * - * @return bool - */ - function needs_shipping_address() { - - $needs_shipping_address = false; - - if ( $this->needs_shipping() === true && ! $this->ship_to_billing_address_only() ) { - $needs_shipping_address = true; - } - - return apply_filters( 'woocommerce_cart_needs_shipping_address', $needs_shipping_address ); - } - - /** - * Sees if the customer has entered enough data to calc the shipping yet. - * - * @return bool - */ - public function show_shipping() { - if ( get_option('woocommerce_calc_shipping') == 'no' || ! is_array( $this->cart_contents ) ) - return false; - - if ( get_option( 'woocommerce_shipping_cost_requires_address' ) == 'yes' ) { - if ( ! WC()->customer->has_calculated_shipping() ) { - if ( ! WC()->customer->get_shipping_country() || ( ! WC()->customer->get_shipping_state() && ! WC()->customer->get_shipping_postcode() ) ) - return false; - } - } - - $show_shipping = true; - - return apply_filters( 'woocommerce_cart_ready_to_calc_shipping', $show_shipping ); - - } - - /** - * Sees if we need a shipping address. - * - * @return bool - */ - public function ship_to_billing_address_only() { - return wc_ship_to_billing_address_only(); - } - - /** - * Gets the shipping total (after calculation). - * - * @return string price or string for the shipping total - */ - public function get_cart_shipping_total() { - if ( isset( $this->shipping_total ) ) { - if ( $this->shipping_total > 0 ) { - - // Display varies depending on settings - if ( $this->tax_display_cart == 'excl' ) { - - $return = wc_price( $this->shipping_total ); - - if ( $this->shipping_tax_total > 0 && $this->prices_include_tax ) { - $return .= ' ' . WC()->countries->ex_tax_or_vat() . ''; - } - - return $return; - - } else { - - $return = wc_price( $this->shipping_total + $this->shipping_tax_total ); - - if ( $this->shipping_tax_total > 0 && ! $this->prices_include_tax ) { - $return .= ' ' . WC()->countries->inc_tax_or_vat() . ''; - } - - return $return; - - } - - } else { - return __( 'Free!', 'woocommerce' ); - } - } - - return ''; - } - - /*-----------------------------------------------------------------------------------*/ - /* Coupons/Discount related functions */ - /*-----------------------------------------------------------------------------------*/ - - /** - * Check for user coupons (now that we have billing email). If a coupon is invalid, add an error. - * - * Checks two types of coupons: - * 1. Where a list of customer emails are set (limits coupon usage to those defined) - * 2. Where a usage_limit_per_user is set (limits coupon usage to a number based on user ID and email) - * - * @access public - * @param array $posted - */ - public function check_customer_coupons( $posted ) { - if ( ! empty( $this->applied_coupons ) ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); - - if ( $coupon->is_valid() ) { - - // Limit to defined email addresses - if ( is_array( $coupon->customer_email ) && sizeof( $coupon->customer_email ) > 0 ) { - $check_emails = array(); - $coupon->customer_email = array_map( 'sanitize_email', $coupon->customer_email ); - - if ( is_user_logged_in() ) { - $current_user = wp_get_current_user(); - $check_emails[] = $current_user->user_email; - } - $check_emails[] = $posted['billing_email']; - $check_emails = array_map( 'sanitize_email', array_map( 'strtolower', $check_emails ) ); - - if ( 0 == sizeof( array_intersect( $check_emails, $coupon->customer_email ) ) ) { - $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ); - - // Remove the coupon - $this->remove_coupon( $code ); - - // Flag totals for refresh - WC()->session->set( 'refresh_totals', true ); - } - } - - // Usage limits per user - check against billing and user email and user ID - if ( $coupon->usage_limit_per_user > 0 ) { - $check_emails = array(); - $used_by = array_filter( (array) get_post_meta( $coupon->id, '_used_by' ) ); - - if ( is_user_logged_in() ) { - $current_user = wp_get_current_user(); - $check_emails[] = sanitize_email( $current_user->user_email ); - $usage_count = sizeof( array_keys( $used_by, get_current_user_id() ) ); - } else { - $check_emails[] = sanitize_email( $posted['billing_email'] ); - $user = get_user_by( 'email', $posted['billing_email'] ); - if ( $user ) { - $usage_count = sizeof( array_keys( $used_by, $user->ID ) ); - } else { - $usage_count = 0; - } - } - - foreach ( $check_emails as $check_email ) { - $usage_count = $usage_count + sizeof( array_keys( $used_by, $check_email ) ); - } - - if ( $usage_count >= $coupon->usage_limit_per_user ) { - $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ); - - // Remove the coupon - $this->remove_coupon( $code ); - - // Flag totals for refresh - WC()->session->set( 'refresh_totals', true ); - } - } - } - } - } - } - - /** - * Returns whether or not a discount has been applied. - * - * @return bool - */ - public function has_discount( $coupon_code ) { - return in_array( apply_filters( 'woocommerce_coupon_code', $coupon_code ), $this->applied_coupons ); - } - - /** - * Applies a coupon code passed to the method. - * - * @param string $coupon_code - The code to apply - * @return bool True if the coupon is applied, false if it does not exist or cannot be applied - */ - public function add_discount( $coupon_code ) { - // Coupons are globally disabled - if ( ! $this->coupons_enabled() ) - return false; - - // Sanitize coupon code - $coupon_code = apply_filters( 'woocommerce_coupon_code', $coupon_code ); - - // Get the coupon - $the_coupon = new WC_Coupon( $coupon_code ); - - if ( $the_coupon->id ) { - - // Check it can be used with cart - if ( ! $the_coupon->is_valid() ) { - wc_add_notice( $the_coupon->get_error_message(), 'error' ); - return false; - } - - // Check if applied - if ( $this->has_discount( $coupon_code ) ) { - $the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED ); - return false; - } - - // If its individual use then remove other coupons - if ( $the_coupon->individual_use == 'yes' ) { - $this->applied_coupons = apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $the_coupon, $this->applied_coupons ); - } - - if ( $this->applied_coupons ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); - - if ( $coupon->individual_use == 'yes' && false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $the_coupon, $coupon, $this->applied_coupons ) ) { - - // Reject new coupon - $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY ); - - return false; - } - } - } - - $this->applied_coupons[] = $coupon_code; - - // Choose free shipping - if ( $the_coupon->enable_free_shipping() ) { - $packages = WC()->shipping->get_packages(); - $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); - - foreach ( $packages as $i => $package ) { - $chosen_shipping_methods[ $i ] = 'free_shipping'; - } - - WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); - } - - $this->calculate_totals(); - - $the_coupon->add_coupon_message( WC_Coupon::WC_COUPON_SUCCESS ); - - do_action( 'woocommerce_applied_coupon', $coupon_code ); - - return true; - - } else { - $the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_EXIST ); - return false; + } catch ( Exception $e ) { + if ( $e->getMessage() ) { + wc_add_notice( $e->getMessage(), 'error' ); } return false; } + } - /** - * Get array of applied coupon objects and codes. - * @param string Type of coupons to get. Can be 'cart' or 'order' which are before and after tax respectively. - * @return array of applied coupons - */ - public function get_coupons( $type = null ) { - $coupons = array(); + /** + * Remove a cart item. + * + * @since 2.3.0 + * @param string $cart_item_key + * @return bool + */ + public function remove_cart_item( $cart_item_key ) { + if ( isset( $this->cart_contents[ $cart_item_key ] ) ) { + $this->removed_cart_contents[ $cart_item_key ] = $this->cart_contents[ $cart_item_key ]; + unset( $this->removed_cart_contents[ $cart_item_key ]['data'] ); - if ( 'cart' == $type || is_null( $type ) ) { - if ( $this->applied_coupons ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); + do_action( 'woocommerce_remove_cart_item', $cart_item_key, $this ); - if ( $coupon->apply_before_tax() ) - $coupons[ $code ] = $coupon; - } - } - } + unset( $this->cart_contents[ $cart_item_key ] ); - if ( 'order' == $type || is_null( $type ) ) { - if ( $this->applied_coupons ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); + do_action( 'woocommerce_cart_item_removed', $cart_item_key, $this ); - if ( ! $coupon->apply_before_tax() ) - $coupons[ $code ] = $coupon; - } - } - } - - return $coupons; - } - - /** - * Gets the array of applied coupon codes. - * - * @return array of applied coupons - */ - public function get_applied_coupons() { - return $this->applied_coupons; - } - - /** - * Get the discount amount for a used coupon - * @param string $code coupon code - * @return float discount amount - */ - public function get_coupon_discount_amount( $code ) { - return isset( $this->coupon_discount_amounts[ $code ] ) ? $this->coupon_discount_amounts[ $code ] : 0; - } - - /** - * Remove coupons from the cart of a defined type. Type 1 is before tax, type 2 is after tax. - * - * @params string type - cart for before tax, order for after tax - */ - public function remove_coupons( $type = null ) { - - if ( 'cart' == $type || 1 == $type ) { - if ( $this->applied_coupons ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); - - if ( $coupon->apply_before_tax() ) - $this->remove_coupon( $code ); - } - } - } elseif ( 'order' == $type || 2 == $type ) { - if ( $this->applied_coupons ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); - - if ( ! $coupon->apply_before_tax() ) - $this->remove_coupon( $code ); - } - } - } else { - $this->applied_coupons = $this->coupon_discount_amounts = $this->coupon_applied_count = array(); - WC()->session->set( 'applied_coupons', array() ); - WC()->session->set( 'coupon_discount_amounts', array() ); - } - } - - /** - * Remove a single coupon by code - * @param string $coupon_code Code of the coupon to remove - * @return bool - */ - public function remove_coupon( $coupon_code ) { - // Coupons are globally disabled - if ( ! $this->coupons_enabled() ) - return false; - - // Get the coupon - $coupon_code = apply_filters( 'woocommerce_coupon_code', $coupon_code ); - $position = array_search( $coupon_code, $this->applied_coupons ); - - if ( $position !== false ) - unset( $this->applied_coupons[ $position ] ); - - WC()->session->set( 'applied_coupons', $this->applied_coupons ); + $this->calculate_totals(); return true; } + return false; + } + + /** + * Restore a cart item. + * + * @param string $cart_item_key + * @return bool + */ + public function restore_cart_item( $cart_item_key ) { + if ( isset( $this->removed_cart_contents[ $cart_item_key ] ) ) { + $this->cart_contents[ $cart_item_key ] = $this->removed_cart_contents[ $cart_item_key ]; + $this->cart_contents[ $cart_item_key ]['data'] = wc_get_product( $this->cart_contents[ $cart_item_key ]['variation_id'] ? $this->cart_contents[ $cart_item_key ]['variation_id'] : $this->cart_contents[ $cart_item_key ]['product_id'] ); + + do_action( 'woocommerce_restore_cart_item', $cart_item_key, $this ); + + unset( $this->removed_cart_contents[ $cart_item_key ] ); + + do_action( 'woocommerce_cart_item_restored', $cart_item_key, $this ); + + $this->calculate_totals(); + + return true; + } + + return false; + } + + /** + * Set the quantity for an item in the cart. + * + * @param string $cart_item_key contains the id of the cart item + * @param int $quantity contains the quantity of the item + * @param bool $refresh_totals whether or not to calculate totals after setting the new qty + * + * @return bool + */ + public function set_quantity( $cart_item_key, $quantity = 1, $refresh_totals = true ) { + if ( 0 == $quantity || $quantity < 0 ) { + do_action( 'woocommerce_before_cart_item_quantity_zero', $cart_item_key ); + unset( $this->cart_contents[ $cart_item_key ] ); + } else { + $old_quantity = $this->cart_contents[ $cart_item_key ]['quantity']; + $this->cart_contents[ $cart_item_key ]['quantity'] = $quantity; + do_action( 'woocommerce_after_cart_item_quantity_update', $cart_item_key, $quantity, $old_quantity ); + } + + if ( $refresh_totals ) { + $this->calculate_totals(); + } + + return true; + } + + /* + * Cart Calculation Functions. + */ + + /** + * Reset cart totals to the defaults. Useful before running calculations. + * + * @param bool $unset_session If true, the session data will be forced unset. + * @access private + */ + private function reset( $unset_session = false ) { + foreach ( $this->cart_session_data as $key => $default ) { + $this->$key = $default; + if ( $unset_session ) { + unset( WC()->session->$key ); + } + } + do_action( 'woocommerce_cart_reset', $this, $unset_session ); + } + + /** + * Sort by subtotal. + * @param array $a + * @param array $b + * @return int + */ + private function sort_by_subtotal( $a, $b ) { + $first_item_subtotal = isset( $a['line_subtotal'] ) ? $a['line_subtotal'] : 0; + $second_item_subtotal = isset( $b['line_subtotal'] ) ? $b['line_subtotal'] : 0; + if ( $first_item_subtotal === $second_item_subtotal ) { + return 0; + } + return ( $first_item_subtotal < $second_item_subtotal ) ? 1 : -1; + } + + /** + * Calculate totals for the items in the cart. + */ + public function calculate_totals() { + $this->reset(); + $this->coupons = $this->get_coupons(); + + do_action( 'woocommerce_before_calculate_totals', $this ); + + if ( $this->is_empty() ) { + $this->set_session(); + return; + } + + $tax_rates = array(); + $shop_tax_rates = array(); + $cart = $this->get_cart(); + /** - * Function to apply discounts to a product and get the discounted price (before tax is applied). - * - * @access public - * @param mixed $values - * @param mixed $price - * @param bool $add_totals (default: false) - * @return float price + * Calculate subtotals for items. This is done first so that discount logic can use the values. */ - public function get_discounted_price( $values, $price, $add_totals = false ) { - if ( ! $price ) - return $price; + foreach ( $cart as $cart_item_key => $values ) { + $product = $values['data']; + $line_price = $product->get_price() * $values['quantity']; + $line_subtotal = 0; + $line_subtotal_tax = 0; - if ( ! empty( $this->applied_coupons ) ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); + /** + * No tax to calculate. + */ + if ( ! $product->is_taxable() ) { - if ( $coupon->apply_before_tax() && $coupon->is_valid() ) { - if ( $coupon->is_valid_for_product( $values['data'] ) || $coupon->is_valid_for_cart() ) { + // Subtotal is the undiscounted price + $this->subtotal += $line_price; + $this->subtotal_ex_tax += $line_price; - $discount_amount = $coupon->get_discount_amount( $price, $values, $single = true ); - $price = max( $price - $discount_amount, 0 ); + /** + * Prices include tax. + * + * To prevent rounding issues we need to work with the inclusive price where possible. + * otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would. + * be 8.325 leading to totals being 1p off. + * + * Pre tax coupons come off the price the customer thinks they are paying - tax is calculated. + * afterwards. + * + * e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that. + */ + } elseif ( $this->prices_include_tax ) { - if ( $add_totals ) { - $this->discount_cart += $discount_amount * $values['quantity']; - $this->increase_coupon_discount_amount( $code, $discount_amount * $values['quantity'] ); - $this->increase_coupon_applied_count( $code, $values['quantity'] ); + // Get base tax rates + if ( empty( $shop_tax_rates[ $product->get_tax_class( 'unfiltered' ) ] ) ) { + $shop_tax_rates[ $product->get_tax_class( 'unfiltered' ) ] = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); + } + + // Get item tax rates + if ( empty( $tax_rates[ $product->get_tax_class() ] ) ) { + $tax_rates[ $product->get_tax_class() ] = WC_Tax::get_rates( $product->get_tax_class() ); + } + + $base_tax_rates = $shop_tax_rates[ $product->get_tax_class( 'unfiltered' ) ]; + $item_tax_rates = $tax_rates[ $product->get_tax_class() ]; + + /** + * ADJUST TAX - Calculations when base tax is not equal to the item tax. + * + * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. + * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. + * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. + */ + if ( $item_tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { + + // Work out a new base price without the shop's base tax + $taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true, true ); + + // Now we have a new item price (excluding TAX) + $line_subtotal = $line_price - array_sum( $taxes ); + + // Now add modified taxes + $tax_result = WC_Tax::calc_tax( $line_subtotal, $item_tax_rates ); + $line_subtotal_tax = array_sum( $tax_result ); + + /** + * Regular tax calculation (customer inside base and the tax class is unmodified. + */ + } else { + + // Calc tax normally + $taxes = WC_Tax::calc_tax( $line_price, $item_tax_rates, true ); + $line_subtotal_tax = array_sum( $taxes ); + $line_subtotal = $line_price - array_sum( $taxes ); + } + + /** + * Prices exclude tax. + * + * This calculation is simpler - work with the base, untaxed price. + */ + } else { + + // Get item tax rates + if ( empty( $tax_rates[ $product->get_tax_class() ] ) ) { + $tax_rates[ $product->get_tax_class() ] = WC_Tax::get_rates( $product->get_tax_class() ); + } + + $item_tax_rates = $tax_rates[ $product->get_tax_class() ]; + + // Base tax for line before discount - we will store this in the order data + $taxes = WC_Tax::calc_tax( $line_price, $item_tax_rates ); + $line_subtotal_tax = array_sum( $taxes ); + + $line_subtotal = $line_price; + } + + // Add to main subtotal + $this->subtotal += $line_subtotal + $line_subtotal_tax; + $this->subtotal_ex_tax += $line_subtotal; + } + + // Order cart items by price so coupon logic is 'fair' for customers and not based on order added to cart. + uasort( $cart, apply_filters( 'woocommerce_sort_by_subtotal_callback', array( $this, 'sort_by_subtotal' ) ) ); + + /** + * Calculate totals for items. + */ + foreach ( $cart as $cart_item_key => $values ) { + + $product = $values['data']; + + // Prices + $base_price = $product->get_price(); + $line_price = $product->get_price() * $values['quantity']; + + // Tax data + $taxes = array(); + $discounted_taxes = array(); + + /** + * No tax to calculate. + */ + if ( ! $product->is_taxable() ) { + + // Discounted Price (price with any pre-tax discounts applied) + $discounted_price = $this->get_discounted_price( $values, $base_price, true ); + $line_subtotal_tax = 0; + $line_subtotal = $line_price; + $line_tax = 0; + $line_total = round( $discounted_price * $values['quantity'], wc_get_rounding_precision() ); + + /** + * Prices include tax. + */ + } elseif ( $this->prices_include_tax ) { + + $base_tax_rates = $shop_tax_rates[ $product->get_tax_class( 'unfiltered' ) ]; + $item_tax_rates = $tax_rates[ $product->get_tax_class() ]; + + /** + * ADJUST TAX - Calculations when base tax is not equal to the item tax. + * + * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. + * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. + * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. + */ + if ( $item_tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { + + // Work out a new base price without the shop's base tax + $taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true, true ); + + // Now we have a new item price (excluding TAX) + $line_subtotal = round( $line_price - array_sum( $taxes ), wc_get_rounding_precision() ); + $taxes = WC_Tax::calc_tax( $line_subtotal, $item_tax_rates ); + $line_subtotal_tax = array_sum( $taxes ); + + // Adjusted price (this is the price including the new tax rate) + $adjusted_price = ( $line_subtotal + $line_subtotal_tax ) / $values['quantity']; + + // Apply discounts and get the discounted price FOR A SINGLE ITEM + $discounted_price = $this->get_discounted_price( $values, $adjusted_price, true ); + + // Convert back to line price + $discounted_line_price = $discounted_price * $values['quantity']; + + // Now use rounded line price to get taxes. + $discounted_taxes = WC_Tax::calc_tax( $discounted_line_price, $item_tax_rates, true ); + $line_tax = array_sum( $discounted_taxes ); + $line_total = $discounted_line_price - $line_tax; + + /** + * Regular tax calculation (customer inside base and the tax class is unmodified. + */ + } else { + + // Work out a new base price without the item tax + $taxes = WC_Tax::calc_tax( $line_price, $item_tax_rates, true ); + + // Now we have a new item price (excluding TAX) + $line_subtotal = $line_price - array_sum( $taxes ); + $line_subtotal_tax = array_sum( $taxes ); + + // Calc prices and tax (discounted) + $discounted_price = $this->get_discounted_price( $values, $base_price, true ); + + // Convert back to line price + $discounted_line_price = $discounted_price * $values['quantity']; + + // Now use rounded line price to get taxes. + $discounted_taxes = WC_Tax::calc_tax( $discounted_line_price, $item_tax_rates, true ); + $line_tax = array_sum( $discounted_taxes ); + $line_total = $discounted_line_price - $line_tax; + } + + // Tax rows - merge the totals we just got + foreach ( array_keys( $this->taxes + $discounted_taxes ) as $key ) { + $this->taxes[ $key ] = ( isset( $discounted_taxes[ $key ] ) ? $discounted_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 ); + } + + /** + * Prices exclude tax. + */ + } else { + + $item_tax_rates = $tax_rates[ $product->get_tax_class() ]; + + // Work out a new base price without the shop's base tax + $taxes = WC_Tax::calc_tax( $line_price, $item_tax_rates ); + + // Now we have the item price (excluding TAX) + $line_subtotal = $line_price; + $line_subtotal_tax = array_sum( $taxes ); + + // Now calc product rates + $discounted_price = $this->get_discounted_price( $values, $base_price, true ); + $discounted_taxes = WC_Tax::calc_tax( $discounted_price * $values['quantity'], $item_tax_rates ); + $discounted_tax_amount = array_sum( $discounted_taxes ); + $line_tax = $discounted_tax_amount; + $line_total = $discounted_price * $values['quantity']; + + // Tax rows - merge the totals we just got + foreach ( array_keys( $this->taxes + $discounted_taxes ) as $key ) { + $this->taxes[ $key ] = ( isset( $discounted_taxes[ $key ] ) ? $discounted_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 ); + } + } + + // Cart contents total is based on discounted prices and is used for the final total calculation + $this->cart_contents_total += $line_total; + + /** + * Store costs + taxes for lines. For tax inclusive prices, we do some extra rounding logic so the stored + * values "add up" when viewing the order in admin. This does have the disadvatage of not being able to + * recalculate the tax total/subtotal accurately in the future, but it does ensure the data looks correct. + * + * Tax exclusive prices are not affected. + */ + if ( ! $product->is_taxable() || $this->prices_include_tax ) { + $this->cart_contents[ $cart_item_key ]['line_total'] = round( $line_total + $line_tax - wc_round_tax_total( $line_tax ), $this->dp ); + $this->cart_contents[ $cart_item_key ]['line_subtotal'] = round( $line_subtotal + $line_subtotal_tax - wc_round_tax_total( $line_subtotal_tax ), $this->dp ); + $this->cart_contents[ $cart_item_key ]['line_tax'] = wc_round_tax_total( $line_tax ); + $this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = wc_round_tax_total( $line_subtotal_tax ); + $this->cart_contents[ $cart_item_key ]['line_tax_data'] = array( 'total' => array_map( 'wc_round_tax_total', $discounted_taxes ), 'subtotal' => array_map( 'wc_round_tax_total', $taxes ) ); + } else { + $this->cart_contents[ $cart_item_key ]['line_total'] = $line_total; + $this->cart_contents[ $cart_item_key ]['line_subtotal'] = $line_subtotal; + $this->cart_contents[ $cart_item_key ]['line_tax'] = $line_tax; + $this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $line_subtotal_tax; + $this->cart_contents[ $cart_item_key ]['line_tax_data'] = array( 'total' => $discounted_taxes, 'subtotal' => $taxes ); + } + } + + // Only calculate the grand total + shipping if on the cart/checkout + if ( is_checkout() || is_cart() || defined( 'WOOCOMMERCE_CHECKOUT' ) || defined( 'WOOCOMMERCE_CART' ) ) { + + // Calculate the Shipping. + $local_pickup_methods = apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ); + $had_local_pickup = 0 < count( array_intersect( wc_get_chosen_shipping_method_ids(), $local_pickup_methods ) ); + $this->calculate_shipping(); + $has_local_pickup = 0 < count( array_intersect( wc_get_chosen_shipping_method_ids(), $local_pickup_methods ) ); + + // If methods changed and local pickup is selected, we need to do a recalculation of taxes. + if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && $had_local_pickup !== $has_local_pickup ) { + return $this->calculate_totals(); + } + + // Trigger the fees API where developers can add fees to the cart + $this->calculate_fees(); + + // Total up/round taxes and shipping taxes + if ( $this->round_at_subtotal ) { + $this->tax_total = WC_Tax::get_tax_total( $this->taxes ); + $this->shipping_tax_total = WC_Tax::get_tax_total( $this->shipping_taxes ); + $this->taxes = array_map( array( 'WC_Tax', 'round' ), $this->taxes ); + $this->shipping_taxes = array_map( array( 'WC_Tax', 'round' ), $this->shipping_taxes ); + } else { + $this->tax_total = array_sum( $this->taxes ); + $this->shipping_tax_total = array_sum( $this->shipping_taxes ); + } + + // VAT exemption done at this point - so all totals are correct before exemption + if ( WC()->customer->get_is_vat_exempt() ) { + $this->remove_taxes(); + } + + // Allow plugins to hook and alter totals before final total is calculated + do_action( 'woocommerce_calculate_totals', $this ); + + // Grand Total - Discounted product prices, discounted tax, shipping cost + tax + $this->total = max( 0, apply_filters( 'woocommerce_calculated_total', round( $this->cart_contents_total + $this->tax_total + $this->shipping_tax_total + $this->shipping_total + $this->fee_total, $this->dp ), $this ) ); + + } else { + + // Set tax total to sum of all tax rows + $this->tax_total = WC_Tax::get_tax_total( $this->taxes ); + + // VAT exemption done at this point - so all totals are correct before exemption + if ( WC()->customer->get_is_vat_exempt() ) { + $this->remove_taxes(); + } + } + + do_action( 'woocommerce_after_calculate_totals', $this ); + + $this->set_session(); + } + + /** + * Remove taxes. + */ + public function remove_taxes() { + $this->shipping_tax_total = $this->tax_total = 0; + $this->subtotal = $this->subtotal_ex_tax; + + foreach ( $this->cart_contents as $cart_item_key => $item ) { + $this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $this->cart_contents[ $cart_item_key ]['line_tax'] = 0; + $this->cart_contents[ $cart_item_key ]['line_tax_data'] = array( 'total' => array(), 'subtotal' => array() ); + } + + // If true, zero rate is applied so '0' tax is displayed on the frontend rather than nothing. + if ( apply_filters( 'woocommerce_cart_remove_taxes_apply_zero_rate', true ) ) { + $this->taxes = $this->shipping_taxes = array( apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) => 0 ); + } else { + $this->taxes = $this->shipping_taxes = array(); + } + } + + /** + * Looks at the totals to see if payment is actually required. + * + * @return bool + */ + public function needs_payment() { + return apply_filters( 'woocommerce_cart_needs_payment', $this->total > 0, $this ); + } + + /* + * Shipping related functions. + */ + + /** + * Uses the shipping class to calculate shipping then gets the totals when its finished. + */ + public function calculate_shipping() { + if ( $this->needs_shipping() && $this->show_shipping() ) { + WC()->shipping->calculate_shipping( $this->get_shipping_packages() ); + } else { + WC()->shipping->reset_shipping(); + } + + // Get totals for the chosen shipping method + $this->shipping_total = WC()->shipping->shipping_total; // Shipping Total + $this->shipping_taxes = WC()->shipping->shipping_taxes; // Shipping Taxes + } + + /** + * Filter items needing shipping callback. + * + * @since 3.0.0 + * @param array $item + * @return bool + */ + protected function filter_items_needing_shipping( $item ) { + $product = $item['data']; + return $product && $product->needs_shipping(); + } + + /** + * Get only items that need shipping. + * + * @since 3.0.0 + * @return array + */ + protected function get_items_needing_shipping() { + return array_filter( $this->get_cart(), array( $this, 'filter_items_needing_shipping' ) ); + } + + /** + * Get packages to calculate shipping for. + * + * This lets us calculate costs for carts that are shipped to multiple locations. + * + * Shipping methods are responsible for looping through these packages. + * + * By default we pass the cart itself as a package - plugins can change this. + * through the filter and break it up. + * + * @since 1.5.4 + * @return array of cart items + */ + public function get_shipping_packages() { + return apply_filters( 'woocommerce_cart_shipping_packages', + array( + array( + 'contents' => $this->get_items_needing_shipping(), + 'contents_cost' => array_sum( wp_list_pluck( $this->get_items_needing_shipping(), 'line_total' ) ), + 'applied_coupons' => $this->applied_coupons, + 'user' => array( + 'ID' => get_current_user_id(), + ), + 'destination' => array( + 'country' => WC()->customer->get_shipping_country(), + 'state' => WC()->customer->get_shipping_state(), + 'postcode' => WC()->customer->get_shipping_postcode(), + 'city' => WC()->customer->get_shipping_city(), + 'address' => WC()->customer->get_shipping_address(), + 'address_2' => WC()->customer->get_shipping_address_2(), + ), + ), + ) + ); + } + + /** + * Looks through the cart to see if shipping is actually required. + * + * @return bool whether or not the cart needs shipping + */ + public function needs_shipping() { + if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( true ) ) { + return false; + } + $needs_shipping = false; + + if ( ! empty( $this->cart_contents ) ) { + foreach ( $this->cart_contents as $cart_item_key => $values ) { + if ( $values['data']->needs_shipping() ) { + $needs_shipping = true; + break; + } + } + } + + return apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping ); + } + + /** + * Should the shipping address form be shown. + * + * @return bool + */ + public function needs_shipping_address() { + + $needs_shipping_address = false; + + if ( $this->needs_shipping() === true && ! wc_ship_to_billing_address_only() ) { + $needs_shipping_address = true; + } + + return apply_filters( 'woocommerce_cart_needs_shipping_address', $needs_shipping_address ); + } + + /** + * Sees if the customer has entered enough data to calc the shipping yet. + * + * @return bool + */ + public function show_shipping() { + if ( ! wc_shipping_enabled() || ! is_array( $this->cart_contents ) ) { + return false; + } + + if ( 'yes' === get_option( 'woocommerce_shipping_cost_requires_address' ) ) { + if ( ! WC()->customer->has_calculated_shipping() ) { + if ( ! WC()->customer->get_shipping_country() || ( ! WC()->customer->get_shipping_state() && ! WC()->customer->get_shipping_postcode() ) ) { + return false; + } + } + } + + return apply_filters( 'woocommerce_cart_ready_to_calc_shipping', true ); + } + + /** + * Sees if we need a shipping address. + * + * @deprecated 2.5.0 in favor to wc_ship_to_billing_address_only() + * + * @return bool + */ + public function ship_to_billing_address_only() { + return wc_ship_to_billing_address_only(); + } + + /** + * Gets the shipping total (after calculation). + * + * @return string price or string for the shipping total + */ + public function get_cart_shipping_total() { + if ( isset( $this->shipping_total ) ) { + if ( $this->shipping_total > 0 ) { + + // Display varies depending on settings + if ( 'excl' === $this->tax_display_cart ) { + + $return = wc_price( $this->shipping_total ); + + if ( $this->shipping_tax_total > 0 && $this->prices_include_tax ) { + $return .= ' ' . WC()->countries->ex_tax_or_vat() . ''; + } + + return $return; + + } else { + + $return = wc_price( $this->shipping_total + $this->shipping_tax_total ); + + if ( $this->shipping_tax_total > 0 && ! $this->prices_include_tax ) { + $return .= ' ' . WC()->countries->inc_tax_or_vat() . ''; + } + + return $return; + + } + } else { + return __( 'Free!', 'woocommerce' ); + } + } + + return ''; + } + + /* + * Coupons/Discount related functions. + */ + + /** + * Check for user coupons (now that we have billing email). If a coupon is invalid, add an error. + * + * Checks two types of coupons: + * 1. Where a list of customer emails are set (limits coupon usage to those defined). + * 2. Where a usage_limit_per_user is set (limits coupon usage to a number based on user ID and email). + * + * @param array $posted + */ + public function check_customer_coupons( $posted ) { + if ( ! empty( $this->applied_coupons ) ) { + foreach ( $this->applied_coupons as $code ) { + $coupon = new WC_Coupon( $code ); + + if ( $coupon->is_valid() ) { + + // Limit to defined email addresses + if ( is_array( $coupon->get_email_restrictions() ) && sizeof( $coupon->get_email_restrictions() ) > 0 ) { + $check_emails = array(); + if ( is_user_logged_in() ) { + $current_user = wp_get_current_user(); + $check_emails[] = $current_user->user_email; + } + $check_emails[] = $posted['billing_email']; + $check_emails = array_map( 'sanitize_email', array_map( 'strtolower', $check_emails ) ); + + if ( 0 == sizeof( array_intersect( $check_emails, $coupon->get_email_restrictions() ) ) ) { + $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ); + + // Remove the coupon + $this->remove_coupon( $code ); + + // Flag totals for refresh + WC()->session->set( 'refresh_totals', true ); + } + } + + // Usage limits per user - check against billing and user email and user ID + if ( $coupon->get_usage_limit_per_user() > 0 ) { + $check_emails = array(); + $used_by = $coupon->get_used_by(); + + if ( is_user_logged_in() ) { + $current_user = wp_get_current_user(); + $check_emails[] = sanitize_email( $current_user->user_email ); + $usage_count = sizeof( array_keys( $used_by, get_current_user_id() ) ); + } else { + $check_emails[] = sanitize_email( $posted['billing_email'] ); + $user = get_user_by( 'email', $posted['billing_email'] ); + if ( $user ) { + $usage_count = sizeof( array_keys( $used_by, $user->ID ) ); + } else { + $usage_count = 0; } } + + foreach ( $check_emails as $check_email ) { + $usage_count = $usage_count + sizeof( array_keys( $used_by, $check_email ) ); + } + + if ( $usage_count >= $coupon->get_usage_limit_per_user() ) { + $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ); + + // Remove the coupon + $this->remove_coupon( $code ); + + // Flag totals for refresh + WC()->session->set( 'refresh_totals', true ); + } } } } + } + } - return apply_filters( 'woocommerce_get_discounted_price', $price, $values, $this ); + /** + * Returns whether or not a discount has been applied. + * @param string $coupon_code + * @return bool + */ + public function has_discount( $coupon_code = '' ) { + return $coupon_code ? in_array( wc_format_coupon_code( $coupon_code ), $this->applied_coupons ) : sizeof( $this->applied_coupons ) > 0; + } + + /** + * Applies a coupon code passed to the method. + * + * @param string $coupon_code - The code to apply + * @return bool True if the coupon is applied, false if it does not exist or cannot be applied + */ + public function add_discount( $coupon_code ) { + // Coupons are globally disabled. + if ( ! wc_coupons_enabled() ) { + return false; } - /** - * Function to apply cart discounts after tax. - * - * @access public - */ - public function apply_cart_discounts_after_tax() { - $pre_discount_total = round( $this->cart_contents_total + $this->tax_total + $this->shipping_tax_total + $this->shipping_total + $this->fee_total, $this->dp ); + // Sanitize coupon code. + $coupon_code = wc_format_coupon_code( $coupon_code ); - if ( $this->applied_coupons ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); + // Get the coupon. + $the_coupon = new WC_Coupon( $coupon_code ); - do_action( 'woocommerce_cart_discount_after_tax_' . $coupon->type, $coupon ); + // Prevent adding coupons by post ID. + if ( $the_coupon->get_code() !== $coupon_code ) { + $the_coupon->set_code( $coupon_code ); + $the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_EXIST ); + return false; + } - if ( $coupon->is_valid() && ! $coupon->apply_before_tax() && $coupon->is_valid_for_cart() ) { - $discount_amount = $coupon->get_discount_amount( $pre_discount_total ); - $pre_discount_total = $pre_discount_total - $discount_amount; - $this->discount_total += $discount_amount; - $this->increase_coupon_discount_amount( $code, $discount_amount ); - $this->increase_coupon_applied_count( $code ); - } + // Check it can be used with cart. + if ( ! $the_coupon->is_valid() ) { + wc_add_notice( $the_coupon->get_error_message(), 'error' ); + return false; + } + + // Check if applied. + if ( $this->has_discount( $coupon_code ) ) { + $the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED ); + return false; + } + + // If its individual use then remove other coupons. + if ( $the_coupon->get_individual_use() ) { + $coupons_to_keep = apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $the_coupon, $this->applied_coupons ); + + foreach ( $this->applied_coupons as $applied_coupon ) { + $keep_key = array_search( $applied_coupon, $coupons_to_keep ); + if ( false === $keep_key ) { + $this->remove_coupon( $applied_coupon ); + } else { + unset( $coupons_to_keep[ $keep_key ] ); + } + } + + if ( ! empty( $coupons_to_keep ) ) { + $this->applied_coupons += $coupons_to_keep; + } + } + + // Check to see if an individual use coupon is set. + if ( $this->applied_coupons ) { + foreach ( $this->applied_coupons as $code ) { + $coupon = new WC_Coupon( $code ); + + if ( $coupon->get_individual_use() && false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $the_coupon, $coupon, $this->applied_coupons ) ) { + + // Reject new coupon. + $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY ); + + return false; } } } - /** - * Function to apply product discounts after tax. - * - * @access public - * @param mixed $values - * @param mixed $price - */ - public function apply_product_discounts_after_tax( $values, $price ) { - if ( ! empty( $this->applied_coupons ) ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); + $this->applied_coupons[] = $coupon_code; - do_action( 'woocommerce_product_discount_after_tax_' . $coupon->type, $coupon, $values, $price ); + // Choose free shipping. + if ( $the_coupon->get_free_shipping() ) { + $packages = WC()->shipping->get_packages(); + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); - if ( $coupon->is_valid() && ! $coupon->apply_before_tax() && $coupon->is_valid_for_product( $values['data'] ) ) { - $discount_amount = $coupon->get_discount_amount( $price, $values ); - $this->discount_total += $discount_amount; - $this->increase_coupon_discount_amount( $code, $discount_amount ); + foreach ( $packages as $i => $package ) { + $chosen_shipping_methods[ $i ] = 'free_shipping'; + } + + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } + + $the_coupon->add_coupon_message( WC_Coupon::WC_COUPON_SUCCESS ); + + do_action( 'woocommerce_applied_coupon', $coupon_code ); + + return true; + } + + /** + * Get array of applied coupon objects and codes. + * + * @param null $deprecated + * + * @return array of applied coupons + */ + public function get_coupons( $deprecated = null ) { + $coupons = array(); + + if ( 'order' === $deprecated ) { + return $coupons; + } + + foreach ( $this->get_applied_coupons() as $code ) { + $coupon = new WC_Coupon( $code ); + $coupons[ $code ] = $coupon; + } + + return $coupons; + } + + /** + * Gets the array of applied coupon codes. + * + * @return array of applied coupons + */ + public function get_applied_coupons() { + return $this->applied_coupons; + } + + /** + * Get the discount amount for a used coupon. + * @param string $code coupon code + * @param bool $ex_tax inc or ex tax + * @return float discount amount + */ + public function get_coupon_discount_amount( $code, $ex_tax = true ) { + $discount_amount = isset( $this->coupon_discount_amounts[ $code ] ) ? $this->coupon_discount_amounts[ $code ] : 0; + + if ( ! $ex_tax ) { + $discount_amount += $this->get_coupon_discount_tax_amount( $code ); + } + + return wc_cart_round_discount( $discount_amount, $this->dp ); + } + + /** + * Get the discount tax amount for a used coupon (for tax inclusive prices). + * @param string $code coupon code + * @param bool inc or ex tax + * @return float discount amount + */ + public function get_coupon_discount_tax_amount( $code ) { + return wc_cart_round_discount( isset( $this->coupon_discount_tax_amounts[ $code ] ) ? $this->coupon_discount_tax_amounts[ $code ] : 0, $this->dp ); + } + + /** + * Remove coupons from the cart of a defined type. Type 1 is before tax, type 2 is after tax. + * + * @param null $deprecated + */ + public function remove_coupons( $deprecated = null ) { + $this->applied_coupons = $this->coupon_discount_amounts = $this->coupon_discount_tax_amounts = $this->coupon_applied_count = array(); + WC()->session->set( 'applied_coupons', array() ); + WC()->session->set( 'coupon_discount_amounts', array() ); + WC()->session->set( 'coupon_discount_tax_amounts', array() ); + } + + /** + * Remove a single coupon by code. + * @param string $coupon_code Code of the coupon to remove + * @return bool + */ + public function remove_coupon( $coupon_code ) { + // Coupons are globally disabled + if ( ! wc_coupons_enabled() ) { + return false; + } + + // Get the coupon + $coupon_code = wc_format_coupon_code( $coupon_code ); + $position = array_search( $coupon_code, $this->applied_coupons ); + + if ( false !== $position ) { + unset( $this->applied_coupons[ $position ] ); + } + + WC()->session->set( 'applied_coupons', $this->applied_coupons ); + + do_action( 'woocommerce_removed_coupon', $coupon_code ); + + return true; + } + + /** + * Function to apply discounts to a product and get the discounted price (before tax is applied). + * + * @param mixed $values + * @param mixed $price + * @param bool $add_totals (default: false) + * @return float price + */ + public function get_discounted_price( $values, $price, $add_totals = false ) { + if ( ! $price ) { + return $price; + } + + $undiscounted_price = $price; + + if ( ! empty( $this->coupons ) ) { + $product = $values['data']; + + foreach ( $this->coupons as $code => $coupon ) { + if ( $coupon->is_valid() && ( $coupon->is_valid_for_product( $product, $values ) || $coupon->is_valid_for_cart() ) ) { + $discount_amount = $coupon->get_discount_amount( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ? $price : $undiscounted_price, $values, true ); + $discount_amount = min( $price, $discount_amount ); + $price = max( $price - $discount_amount, 0 ); + + // Store the totals for DISPLAY in the cart. + if ( $add_totals ) { + $total_discount = $discount_amount * $values['quantity']; + $total_discount_tax = 0; + + if ( wc_tax_enabled() && $product->is_taxable() ) { + $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); + $taxes = WC_Tax::calc_tax( $discount_amount, $tax_rates, $this->prices_include_tax ); + $total_discount_tax = WC_Tax::get_tax_total( $taxes ) * $values['quantity']; + $total_discount = $this->prices_include_tax ? $total_discount - $total_discount_tax : $total_discount; + $this->discount_cart_tax += $total_discount_tax; + } + + $this->discount_cart += $total_discount; + $this->increase_coupon_discount_amount( $code, $total_discount, $total_discount_tax ); $this->increase_coupon_applied_count( $code, $values['quantity'] ); } } - } - } - /** - * Store how much discount each coupon grants. - * - * @access private - * @param mixed $code - * @param mixed $amount - */ - private function increase_coupon_discount_amount( $code, $amount ) { - if ( empty( $this->coupon_discount_amounts[ $code ] ) ) - $this->coupon_discount_amounts[ $code ] = 0; - - $this->coupon_discount_amounts[ $code ] += $amount; - } - - /** - * Store how many times each coupon is applied to cart/items - * - * @access private - * @param mixed $code - * @param mixed $amount - */ - private function increase_coupon_applied_count( $code, $count = 1 ) { - if ( empty( $this->coupon_applied_count[ $code ] ) ) - $this->coupon_applied_count[ $code ] = 0; - - $this->coupon_applied_count[ $code ] += $count; - } - - /*-----------------------------------------------------------------------------------*/ - /* Fees API to add additional costs to orders */ - /*-----------------------------------------------------------------------------------*/ - - /** - * add_fee function. - * - * @param mixed $name - * @param mixed $amount - * @param bool $taxable (default: false) - * @param string $tax_class (default: '') - */ - public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) { - - $new_fee_id = sanitize_title( $name ); - - // Only add each fee once - foreach ( $this->fees as $fee ) { - if ( $fee->id == $new_fee_id ) { - return; + // If the price is 0, we can stop going through coupons because there is nothing more to discount for this product. + if ( 0 >= $price ) { + break; } } - - $new_fee = new stdClass(); - $new_fee->id = $new_fee_id; - $new_fee->name = esc_attr( $name ); - $new_fee->amount = (float) esc_attr( $amount ); - $new_fee->tax_class = $tax_class; - $new_fee->taxable = $taxable ? true : false; - $new_fee->tax = 0; - $this->fees[] = $new_fee; } - /** - * get_fees function. - * - * @access public - * @return array - */ - public function get_fees() { - return array_filter( (array) $this->fees ); + return apply_filters( 'woocommerce_get_discounted_price', $price, $values, $this ); + } + + /** + * Store how much discount each coupon grants. + * + * @access private + * @param string $code + * @param double $amount + * @param double $tax + */ + private function increase_coupon_discount_amount( $code, $amount, $tax ) { + $this->coupon_discount_amounts[ $code ] = isset( $this->coupon_discount_amounts[ $code ] ) ? $this->coupon_discount_amounts[ $code ] + $amount : $amount; + $this->coupon_discount_tax_amounts[ $code ] = isset( $this->coupon_discount_tax_amounts[ $code ] ) ? $this->coupon_discount_tax_amounts[ $code ] + $tax : $tax; + } + + /** + * Store how many times each coupon is applied to cart/items. + * + * @access private + * @param string $code + * @param int $count + */ + private function increase_coupon_applied_count( $code, $count = 1 ) { + if ( empty( $this->coupon_applied_count[ $code ] ) ) { + $this->coupon_applied_count[ $code ] = 0; + } + $this->coupon_applied_count[ $code ] += $count; + } + + /* + * Fees API to add additional costs to orders. + */ + + /** + * Add additional fee to the cart. + * + * Fee is an amount of money charged for a particular piece of work + * or for a particular right or service, and not supposed to be negative. + * + * @param string $name Unique name for the fee. Multiple fees of the same name cannot be added. + * @param float $amount Fee amount (do not enter negative amounts). + * @param bool $taxable Is the fee taxable? (default: false). + * @param string $tax_class The tax class for the fee if taxable. A blank string is standard tax class. (default: ''). + */ + public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) { + + $new_fee_id = sanitize_title( $name ); + + // Only add each fee once. + foreach ( $this->fees as $fee ) { + if ( $fee->id == $new_fee_id ) { + return; + } } - /** - * Calculate fees - */ - public function calculate_fees() { + $new_fee = new stdClass(); + $new_fee->id = $new_fee_id; + $new_fee->name = esc_attr( $name ); + $new_fee->amount = (float) esc_attr( $amount ); + $new_fee->tax_class = $tax_class; + $new_fee->taxable = $taxable ? true : false; + $new_fee->tax = 0; + $new_fee->tax_data = array(); + $this->fees[] = $new_fee; + } - // Fire an action where developers can add their fees - do_action( 'woocommerce_cart_calculate_fees', $this ); + /** + * Get fees. + * + * @return array + */ + public function get_fees() { + return array_filter( (array) $this->fees ); + } - // If fees were added, total them and calculate tax - if ( ! empty( $this->fees ) ) { - foreach ( $this->fees as $fee_key => $fee ) { - $this->fee_total += $fee->amount; + /** + * Calculate fees. + */ + public function calculate_fees() { + // Reset fees before calculation + $this->fee_total = 0; + $this->fees = array(); - if ( $fee->taxable ) { - // Get tax rates - $tax_rates = $this->tax->get_rates( $fee->tax_class ); - $fee_taxes = $this->tax->calc_tax( $fee->amount, $tax_rates, false ); - - if ( ! empty( $fee_taxes ) ) { - // Set the tax total for this fee - $this->fees[ $fee_key ]->tax = array_sum( $fee_taxes ); + // Fire an action where developers can add their fees + do_action( 'woocommerce_cart_calculate_fees', $this ); - // Tax rows - merge the totals we just got - foreach ( array_keys( $this->taxes + $fee_taxes ) as $key ) { - $this->taxes[ $key ] = ( isset( $fee_taxes[ $key ] ) ? $fee_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 ); - } + // If fees were added, total them and calculate tax + if ( ! empty( $this->fees ) ) { + foreach ( $this->fees as $fee_key => $fee ) { + $this->fee_total += $fee->amount; + + if ( $fee->taxable ) { + // Get tax rates + $tax_rates = WC_Tax::get_rates( $fee->tax_class ); + $fee_taxes = WC_Tax::calc_tax( $fee->amount, $tax_rates, false ); + + if ( ! empty( $fee_taxes ) ) { + // Set the tax total for this fee + $this->fees[ $fee_key ]->tax = array_sum( $fee_taxes ); + + // Set tax data - Since 2.2 + $this->fees[ $fee_key ]->tax_data = $fee_taxes; + + // Tax rows - merge the totals we just got + foreach ( array_keys( $this->taxes + $fee_taxes ) as $key ) { + $this->taxes[ $key ] = ( isset( $fee_taxes[ $key ] ) ? $fee_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 ); } } } } } + } - /*-----------------------------------------------------------------------------------*/ - /* Get Formatted Totals */ - /*-----------------------------------------------------------------------------------*/ + /* + * Get Formatted Totals. + */ - /** - * Get the total of all order discounts (after tax discounts). - * - * @return float - */ - public function get_order_discount_total() { - return $this->discount_total; + /** + * Gets the order total (after calculation). + * + * @return string formatted price + */ + public function get_total() { + return apply_filters( 'woocommerce_cart_total', wc_price( $this->total ) ); + } + + /** + * Gets the total excluding taxes. + * + * @return string formatted price + */ + public function get_total_ex_tax() { + $total = $this->total - $this->tax_total - $this->shipping_tax_total; + if ( $total < 0 ) { + $total = 0; + } + return apply_filters( 'woocommerce_cart_total_ex_tax', wc_price( $total ) ); + } + + /** + * Gets the cart contents total (after calculation). + * + * @todo deprecate? It's unused. + * @return string formatted price + */ + public function get_cart_total() { + if ( ! $this->prices_include_tax ) { + $cart_contents_total = wc_price( $this->cart_contents_total ); + } else { + $cart_contents_total = wc_price( $this->cart_contents_total + $this->tax_total ); } - /** - * Get the total of all cart discounts (before tax discounts). - * - * @return float - */ - public function get_cart_discount_total() { - return $this->discount_cart; - } + return apply_filters( 'woocommerce_cart_contents_total', $cart_contents_total ); + } - /** - * Gets the order total (after calculation). - * - * @return string formatted price - */ - public function get_total() { - return apply_filters( 'woocommerce_cart_total', wc_price( $this->total ) ); - } + /** + * Gets the sub total (after calculation). + * + * @param bool $compound whether to include compound taxes + * @return string formatted price + */ + public function get_cart_subtotal( $compound = false ) { - /** - * Gets the total excluding taxes. - * - * @return string formatted price - */ - public function get_total_ex_tax() { - $total = $this->total - $this->tax_total - $this->shipping_tax_total; - if ( $total < 0 ) - $total = 0; - return apply_filters( 'woocommerce_cart_total_ex_tax', wc_price( $total ) ); - } + // If the cart has compound tax, we want to show the subtotal as + // cart + shipping + non-compound taxes (after discount) + if ( $compound ) { - /** - * Gets the cart contents total (after calculation). - * - * @return string formatted price - */ - public function get_cart_total() { - if ( ! $this->prices_include_tax ) { - $cart_contents_total = wc_price( $this->cart_contents_total ); - } else { - $cart_contents_total = wc_price( $this->cart_contents_total + $this->tax_total ); - } + $cart_subtotal = wc_price( $this->cart_contents_total + $this->shipping_total + $this->get_taxes_total( false, false ) ); - return apply_filters( 'woocommerce_cart_contents_total', $cart_contents_total ); - } + // Otherwise we show cart items totals only (before discount) + } else { - /** - * Gets the sub total (after calculation). - * - * @params bool whether to include compound taxes - * @return string formatted price - */ - public function get_cart_subtotal( $compound = false ) { + // Display varies depending on settings + if ( 'excl' === $this->tax_display_cart ) { - // If the cart has compound tax, we want to show the subtotal as - // cart + shipping + non-compound taxes (after discount) - if ( $compound ) { + $cart_subtotal = wc_price( $this->subtotal_ex_tax ); - $cart_subtotal = wc_price( $this->cart_contents_total + $this->shipping_total + $this->get_taxes_total( false, false ) ); - - // Otherwise we show cart items totals only (before discount) + if ( $this->tax_total > 0 && $this->prices_include_tax ) { + $cart_subtotal .= ' ' . WC()->countries->ex_tax_or_vat() . ''; + } } else { - // Display varies depending on settings - if ( $this->tax_display_cart == 'excl' ) { - - $cart_subtotal = wc_price( $this->subtotal_ex_tax ); - - if ( $this->tax_total > 0 && $this->prices_include_tax ) { - $cart_subtotal .= ' ' . WC()->countries->ex_tax_or_vat() . ''; - } - - } else { - - $cart_subtotal = wc_price( $this->subtotal ); - - if ( $this->tax_total > 0 && !$this->prices_include_tax ) { - $cart_subtotal .= ' ' . WC()->countries->inc_tax_or_vat() . ''; - } + $cart_subtotal = wc_price( $this->subtotal ); + if ( $this->tax_total > 0 && ! $this->prices_include_tax ) { + $cart_subtotal .= ' ' . WC()->countries->inc_tax_or_vat() . ''; } } - - return apply_filters( 'woocommerce_cart_subtotal', $cart_subtotal, $compound, $this ); } - /** - * Get the product row price per item. - * - * @param WC_Product $_product - * @return string formatted price - */ - public function get_product_price( $_product ) { - if ( $this->tax_display_cart == 'excl' ) - $product_price = $_product->get_price_excluding_tax(); - else - $product_price = $_product->get_price_including_tax(); + return apply_filters( 'woocommerce_cart_subtotal', $cart_subtotal, $compound, $this ); + } - return apply_filters( 'woocommerce_cart_product_price', wc_price( $product_price ), $_product ); + /** + * Get the product row price per item. + * + * @param WC_Product $product + * @return string formatted price + */ + public function get_product_price( $product ) { + if ( 'excl' === $this->tax_display_cart ) { + $product_price = wc_get_price_excluding_tax( $product ); + } else { + $product_price = wc_get_price_including_tax( $product ); } + return apply_filters( 'woocommerce_cart_product_price', wc_price( $product_price ), $product ); + } - /** - * Get the product row subtotal. - * - * Gets the tax etc to avoid rounding issues. - * - * When on the checkout (review order), this will get the subtotal based on the customer's tax rate rather than the base rate - * - * @param WC_Product $_product - * @param int quantity - * @return string formatted price - */ - public function get_product_subtotal( $_product, $quantity ) { + /** + * Get the product row subtotal. + * + * Gets the tax etc to avoid rounding issues. + * + * When on the checkout (review order), this will get the subtotal based on the customer's tax rate rather than the base rate. + * + * @param WC_Product $product + * @param int $quantity + * @return string formatted price + */ + public function get_product_subtotal( $product, $quantity ) { + $price = $product->get_price(); + $taxable = $product->is_taxable(); - $price = $_product->get_price(); - $taxable = $_product->is_taxable(); + // Taxable + if ( $taxable ) { - // Taxable - if ( $taxable ) { + if ( 'excl' === $this->tax_display_cart ) { - if ( $this->tax_display_cart == 'excl' ) { - - $row_price = $_product->get_price_excluding_tax( $quantity ); - $product_subtotal = wc_price( $row_price ); - - if ( $this->prices_include_tax && $this->tax_total > 0 ) - $product_subtotal .= ' ' . WC()->countries->ex_tax_or_vat() . ''; - - } else { - - $row_price = $_product->get_price_including_tax( $quantity ); - $product_subtotal = wc_price( $row_price ); - - if ( ! $this->prices_include_tax && $this->tax_total > 0 ) - $product_subtotal .= ' ' . WC()->countries->inc_tax_or_vat() . ''; - - } - - // Non-taxable - } else { - - $row_price = $price * $quantity; + $row_price = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); $product_subtotal = wc_price( $row_price ); - } - - return apply_filters( 'woocommerce_cart_product_subtotal', $product_subtotal, $_product, $quantity, $this ); - } - - /** - * Gets the cart tax (after calculation). - * - * @return string formatted price - */ - public function get_cart_tax() { - $cart_total_tax = wc_round_tax_total( $this->tax_total + $this->shipping_tax_total ); - - return apply_filters( 'woocommerce_get_cart_tax', $cart_total_tax ? wc_price( $cart_total_tax ) : '' ); - } - - /** - * Get a tax amount - * @param string $tax_rate_id - * @return float amount - */ - public function get_tax_amount( $tax_rate_id ) { - return isset( $this->taxes[ $tax_rate_id ] ) ? $this->taxes[ $tax_rate_id ] : 0; - } - - /** - * Get a tax amount - * @param string $tax_rate_id - * @return float amount - */ - public function get_shipping_tax_amount( $tax_rate_id ) { - return isset( $this->shipping_taxes[ $tax_rate_id ] ) ? $this->shipping_taxes[ $tax_rate_id ] : 0; - } - - /** - * Get tax row amounts with or without compound taxes includes. - * - * @param boolean $compound True if getting compound taxes - * @param boolean $display True if getting total to display - * @return float price - */ - public function get_taxes_total( $compound = true, $display = true ) { - $total = 0; - foreach ( $this->taxes as $key => $tax ) { - if ( ! $compound && $this->tax->is_compound( $key ) ) continue; - $total += $tax; - } - foreach ( $this->shipping_taxes as $key => $tax ) { - if ( ! $compound && $this->tax->is_compound( $key ) ) continue; - $total += $tax; - } - if ( $display ) { - $total = wc_round_tax_total( $total ); - } - return apply_filters( 'woocommerce_cart_taxes_total', $total, $compound, $display, $this ); - } - - /** - * Gets the total (product) discount amount - these are applied before tax. - * - * @return mixed formatted price or false if there are none - */ - public function get_discounts_before_tax() { - if ( $this->discount_cart ) { - $discounts_before_tax = wc_price( $this->discount_cart ); + if ( $this->prices_include_tax && $this->tax_total > 0 ) { + $product_subtotal .= ' ' . WC()->countries->ex_tax_or_vat() . ''; + } } else { - $discounts_before_tax = false; + + $row_price = wc_get_price_including_tax( $product, array( 'qty' => $quantity ) ); + $product_subtotal = wc_price( $row_price ); + + if ( ! $this->prices_include_tax && $this->tax_total > 0 ) { + $product_subtotal .= ' ' . WC()->countries->inc_tax_or_vat() . ''; + } } - return apply_filters( 'woocommerce_cart_discounts_before_tax', $discounts_before_tax, $this ); + + // Non-taxable + } else { + + $row_price = $price * $quantity; + $product_subtotal = wc_price( $row_price ); + } - /** - * Gets the order discount amount - these are applied after tax. - * - * @return mixed formatted price or false if there are none - */ - public function get_discounts_after_tax() { - if ( $this->discount_total ) { - $discounts_after_tax = wc_price( $this->discount_total ); - } else { - $discounts_after_tax = false; - } - return apply_filters( 'woocommerce_cart_discounts_after_tax', $discounts_after_tax, $this ); - } + return apply_filters( 'woocommerce_cart_product_subtotal', $product_subtotal, $product, $quantity, $this ); + } - /** - * Gets the total discount amount - both kinds. - * - * @return mixed formatted price or false if there are none - */ - public function get_total_discount() { - if ( $this->discount_total || $this->discount_cart ) { - $total_discount = wc_price( $this->discount_total + $this->discount_cart ); - } else { - $total_discount = false; + /** + * Gets the cart tax (after calculation). + * + * @return string formatted price + */ + public function get_cart_tax() { + $cart_total_tax = wc_round_tax_total( $this->tax_total + $this->shipping_tax_total ); + + return apply_filters( 'woocommerce_get_cart_tax', $cart_total_tax ? wc_price( $cart_total_tax ) : '' ); + } + + /** + * Get a tax amount. + * @param string $tax_rate_id + * @return float amount + */ + public function get_tax_amount( $tax_rate_id ) { + return isset( $this->taxes[ $tax_rate_id ] ) ? $this->taxes[ $tax_rate_id ] : 0; + } + + /** + * Get a tax amount. + * @param string $tax_rate_id + * @return float amount + */ + public function get_shipping_tax_amount( $tax_rate_id ) { + return isset( $this->shipping_taxes[ $tax_rate_id ] ) ? $this->shipping_taxes[ $tax_rate_id ] : 0; + } + + /** + * Get tax row amounts with or without compound taxes includes. + * + * @param bool $compound True if getting compound taxes + * @param bool $display True if getting total to display + * @return float price + */ + public function get_taxes_total( $compound = true, $display = true ) { + $total = 0; + foreach ( $this->taxes as $key => $tax ) { + if ( ! $compound && WC_Tax::is_compound( $key ) ) { + continue; } - return apply_filters( 'woocommerce_cart_total_discount', $total_discount, $this ); + $total += $tax; } + foreach ( $this->shipping_taxes as $key => $tax ) { + if ( ! $compound && WC_Tax::is_compound( $key ) ) { + continue; + } + $total += $tax; + } + if ( $display ) { + $total = wc_round_tax_total( $total ); + } + return apply_filters( 'woocommerce_cart_taxes_total', $total, $compound, $display, $this ); + } + + /** + * Get the total of all cart discounts. + * + * @return float + */ + public function get_cart_discount_total() { + return wc_cart_round_discount( $this->discount_cart, $this->dp ); + } + + /** + * Get the total of all cart tax discounts (used for discounts on tax inclusive prices). + * + * @return float + */ + public function get_cart_discount_tax_total() { + return wc_cart_round_discount( $this->discount_cart_tax, $this->dp ); + } + + /** + * Gets the total discount amount - both kinds. + * + * @return mixed formatted price or false if there are none + */ + public function get_total_discount() { + if ( $this->get_cart_discount_total() ) { + $total_discount = wc_price( $this->get_cart_discount_total() ); + } else { + $total_discount = false; + } + return apply_filters( 'woocommerce_cart_total_discount', $total_discount, $this ); + } + + /** + * Gets the total (product) discount amount - these are applied before tax. + * + * @deprecated Order discounts (after tax) removed in 2.3 so multiple methods for discounts are no longer required. + * @return mixed formatted price or false if there are none + */ + public function get_discounts_before_tax() { + wc_deprecated_function( 'get_discounts_before_tax', '2.3', 'get_total_discount' ); + if ( $this->get_cart_discount_total() ) { + $discounts_before_tax = wc_price( $this->get_cart_discount_total() ); + } else { + $discounts_before_tax = false; + } + return apply_filters( 'woocommerce_cart_discounts_before_tax', $discounts_before_tax, $this ); + } + + /** + * Get the total of all order discounts (after tax discounts). + * + * @deprecated Order discounts (after tax) removed in 2.3 + * @return int + */ + public function get_order_discount_total() { + wc_deprecated_function( 'get_order_discount_total', '2.3' ); + return 0; + } + + /** + * Function to apply cart discounts after tax. + * @deprecated Coupons can not be applied after tax + * + * @param $values + * @param $price + */ + public function apply_cart_discounts_after_tax( $values, $price ) { + wc_deprecated_function( 'apply_cart_discounts_after_tax', '2.3' ); + } + + /** + * Function to apply product discounts after tax. + * @deprecated Coupons can not be applied after tax + * + * @param $values + * @param $price + */ + public function apply_product_discounts_after_tax( $values, $price ) { + wc_deprecated_function( 'apply_product_discounts_after_tax', '2.3' ); + } + + /** + * Gets the order discount amount - these are applied after tax. + * @deprecated Coupons can not be applied after tax + */ + public function get_discounts_after_tax() { + wc_deprecated_function( 'get_discounts_after_tax', '2.3' ); + } } diff --git a/includes/class-wc-checkout.php b/includes/class-wc-checkout.php index 6208ff0fc5c..7e240eea05d 100644 --- a/includes/class-wc-checkout.php +++ b/includes/class-wc-checkout.php @@ -1,764 +1,1036 @@ is_registration_enabled() ) { + remove_filter( 'woocommerce_checkout_registration_enabled', '__return_true', 0 ); + remove_filter( 'woocommerce_checkout_registration_enabled', '__return_false', 0 ); + add_filter( 'woocommerce_checkout_registration_enabled', $bool_value ? '__return_true' : '__return_false', 0 ); + } + break; + case 'enable_guest_checkout' : + $bool_value = wc_string_to_bool( $value ); + + if ( $bool_value === $this->is_registration_required() ) { + remove_filter( 'woocommerce_checkout_registration_required', '__return_true', 0 ); + remove_filter( 'woocommerce_checkout_registration_required', '__return_false', 0 ); + add_filter( 'woocommerce_checkout_registration_required', $bool_value ? '__return_false' : '__return_true', 0 ); + } + break; + case 'checkout_fields' : + $this->fields = $value; + break; + case 'shipping_methods' : + WC()->session->set( 'chosen_shipping_methods', $value ); + break; + case 'posted' : + $this->legacy_posted_data = $value; + break; + } + } + + /** + * Gets the legacy public variables for backwards compatibility. + * + * @param string $key + * + * @return array|string + */ + public function __get( $key ) { + if ( in_array( $key, array( 'posted', 'shipping_method', 'payment_method' ) ) && empty( $this->legacy_posted_data ) ) { + $this->legacy_posted_data = $this->get_posted_data(); + } + switch ( $key ) { + case 'enable_signup' : + return $this->is_registration_enabled(); + case 'enable_guest_checkout' : + return ! $this->is_registration_required(); + case 'must_create_account' : + return $this->is_registration_required() && ! is_user_logged_in(); + case 'checkout_fields' : + return $this->get_checkout_fields(); + case 'posted' : + wc_doing_it_wrong( 'WC_Checkout->posted', 'Use $_POST directly.', '3.0.0' ); + return $this->legacy_posted_data; + case 'shipping_method' : + return $this->legacy_posted_data['shipping_method']; + case 'payment_method' : + return $this->legacy_posted_data['payment_method']; + case 'customer_id' : + return apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ); + case 'shipping_methods' : + return (array) WC()->session->get( 'chosen_shipping_methods' ); + } } /** * Cloning is forbidden. - * - * @since 2.1 */ public function __clone() { - _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); + wc_doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); } /** * Unserializing instances of this class is forbidden. - * - * @since 2.1 */ public function __wakeup() { - _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); + wc_doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); } /** - * Constructor for the checkout class. Hooks in methods and defines checkout fields. + * Is registration required to checkout? * - * @access public - * @return void + * @since 3.0.0 + * @return boolean */ - public function __construct () { - add_action( 'woocommerce_checkout_billing', array( $this,'checkout_form_billing' ) ); - add_action( 'woocommerce_checkout_shipping', array( $this,'checkout_form_shipping' ) ); - - $this->enable_signup = get_option( 'woocommerce_enable_signup_and_login_from_checkout' ) == 'yes' ? true : false; - $this->enable_guest_checkout = get_option( 'woocommerce_enable_guest_checkout' ) == 'yes' ? true : false; - $this->must_create_account = $this->enable_guest_checkout || is_user_logged_in() ? false : true; - - // Define all Checkout fields - $this->checkout_fields['billing'] = WC()->countries->get_address_fields( $this->get_value('billing_country'), 'billing_' ); - $this->checkout_fields['shipping'] = WC()->countries->get_address_fields( $this->get_value('shipping_country'), 'shipping_' ); - - if ( get_option( 'woocommerce_registration_generate_username' ) == 'no' ) { - $this->checkout_fields['account']['account_username'] = array( - 'type' => 'text', - 'label' => __( 'Account username', 'woocommerce' ), - 'required' => true, - 'placeholder' => _x( 'Username', 'placeholder', 'woocommerce' ) - ); - } - - if ( get_option( 'woocommerce_registration_generate_password' ) == 'no' ) { - $this->checkout_fields['account']['account_password'] = array( - 'type' => 'password', - 'label' => __( 'Account password', 'woocommerce' ), - 'required' => true, - 'placeholder' => _x( 'Password', 'placeholder', 'woocommerce' ) - ); - } - - $this->checkout_fields['order'] = array( - 'order_comments' => array( - 'type' => 'textarea', - 'class' => array('notes'), - 'label' => __( 'Order Notes', 'woocommerce' ), - 'placeholder' => _x('Notes about your order, e.g. special notes for delivery.', 'placeholder', 'woocommerce') - ) - ); - - $this->checkout_fields = apply_filters( 'woocommerce_checkout_fields', $this->checkout_fields ); - - do_action( 'woocommerce_checkout_init', $this ); + public function is_registration_required() { + return apply_filters( 'woocommerce_checkout_registration_required', 'yes' !== get_option( 'woocommerce_enable_guest_checkout' ) ); } /** - * Checkout process + * Is registration enabled on the checkout page? + * + * @since 3.0.0 + * @return boolean + */ + public function is_registration_enabled() { + return apply_filters( 'woocommerce_checkout_registration_enabled', 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout' ) ); + } + + /** + * Get an array of checkout fields. + * + * @param string $fieldset to get. + * @return array + */ + public function get_checkout_fields( $fieldset = '' ) { + if ( is_null( $this->fields ) ) { + $this->fields = array( + 'billing' => WC()->countries->get_address_fields( $this->get_value( 'billing_country' ), 'billing_' ), + 'shipping' => WC()->countries->get_address_fields( $this->get_value( 'shipping_country' ), 'shipping_' ), + 'account' => array(), + 'order' => array( + 'order_comments' => array( + 'type' => 'textarea', + 'class' => array( 'notes' ), + 'label' => __( 'Order notes', 'woocommerce' ), + 'placeholder' => esc_attr__( 'Notes about your order, e.g. special notes for delivery.', 'woocommerce' ), + ), + ), + ); + if ( 'no' === get_option( 'woocommerce_registration_generate_username' ) ) { + $this->fields['account']['account_username'] = array( + 'type' => 'text', + 'label' => __( 'Account username', 'woocommerce' ), + 'required' => true, + 'placeholder' => esc_attr__( 'Username', 'woocommerce' ), + ); + } + + if ( 'no' === get_option( 'woocommerce_registration_generate_password' ) ) { + $this->fields['account']['account_password'] = array( + 'type' => 'password', + 'label' => __( 'Account password', 'woocommerce' ), + 'required' => true, + 'placeholder' => esc_attr__( 'Password', 'woocommerce' ), + ); + } + + $this->fields = apply_filters( 'woocommerce_checkout_fields', $this->fields ); + } + if ( $fieldset ) { + return $this->fields[ $fieldset ]; + } else { + return $this->fields; + } + } + + /** + * When we process the checkout, lets ensure cart items are rechecked to prevent checkout. */ public function check_cart_items() { - // When we process the checkout, lets ensure cart items are rechecked to prevent checkout - do_action('woocommerce_check_cart_items'); + do_action( 'woocommerce_check_cart_items' ); } /** - * Output the billing information form - * - * @access public - * @return void + * Output the billing form. */ public function checkout_form_billing() { wc_get_template( 'checkout/form-billing.php', array( 'checkout' => $this ) ); } /** - * Output the shipping information form - * - * @access public - * @return void + * Output the shipping form. */ public function checkout_form_shipping() { wc_get_template( 'checkout/form-shipping.php', array( 'checkout' => $this ) ); } /** - * create_order function. - * @access public + * Create an order. Error codes: + * 520 - Cannot insert order into the database. + * 521 - Cannot get order after creation. + * 522 - Cannot update order. + * 525 - Cannot create line item. + * 526 - Cannot create fee item. + * 527 - Cannot create shipping item. + * 528 - Cannot create tax item. + * 529 - Cannot create coupon item. + * * @throws Exception + * @param $data Posted data. * @return int|WP_ERROR */ - public function create_order() { - global $wpdb; - - // Give plugins the opportunity to create an order themselves + public function create_order( $data ) { + // Give plugins the opportunity to create an order themselves. if ( $order_id = apply_filters( 'woocommerce_create_order', null, $this ) ) { return $order_id; } try { - // Start transaction if available - $wpdb->query( 'START TRANSACTION' ); + $order_id = absint( WC()->session->get( 'order_awaiting_payment' ) ); + $cart_hash = md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total ); + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - $order_data = array( - 'status' => apply_filters( 'woocommerce_default_order_status', 'pending' ), - 'customer_id' => $this->customer_id, - 'customer_note' => isset( $this->posted['order_comments'] ) ? $this->posted['order_comments'] : '' - ); - - // Insert or update the post data - $order_id = absint( WC()->session->order_awaiting_payment ); - - // Resume the unpaid order if its pending - if ( $order_id > 0 && ( $order = new WC_Order( $order_id ) ) && $order->has_status( array( 'pending', 'failed' ) ) ) { - - $order_data['order_id'] = $order_id; - $order = wc_update_order( $order_data ); - - if ( is_wp_error( $order ) ) { - throw new Exception( __( 'Error: Unable to create order. Please try again.', 'woocommerce' ) ); - } else { - $order->remove_order_items(); - do_action( 'woocommerce_resume_order', $order_id ); - } + /** + * If there is an order pending payment, we can resume it here so + * long as it has not changed. If the order has changed, i.e. + * different items or cost, create a new order. We use a hash to + * detect changes which is based on cart items + order total. + */ + if ( $order_id && ( $order = wc_get_order( $order_id ) ) && $order->has_cart_hash( $cart_hash ) && $order->has_status( array( 'pending', 'failed' ) ) ) { + // Action for 3rd parties. + do_action( 'woocommerce_resume_order', $order_id ); + // Remove all items - we will re-add them later. + $order->remove_order_items(); } else { + $order = new WC_Order(); + } - $order = wc_create_order( $order_data ); + foreach ( $data as $key => $value ) { + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); - if ( is_wp_error( $order ) ) { - throw new Exception( __( 'Error: Unable to create order. Please try again.', 'woocommerce' ) ); - } else { - $order_id = $order->id; - do_action( 'woocommerce_new_order', $order_id ); + // Store custom fields prefixed with wither shipping_ or billing_. This is for backwards compatibility with 2.6.x. + } elseif ( 0 === stripos( $key, 'billing_' ) || 0 === stripos( $key, 'shipping_' ) ) { + $order->update_meta_data( '_' . $key, $value ); } } - // Store the line items to the new/resumed order - foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { - $item_id = $order->add_product( - $values['data'], - $values['quantity'], - array( - 'variation' => $values['variation'], - 'totals' => array( - 'subtotal' => $values['line_subtotal'], - 'subtotal_tax' => $values['line_subtotal_tax'], - 'total' => $values['line_total'], - 'tax' => $values['line_tax'] - ) - ) - ); - - if ( ! $item_id ) { - throw new Exception( __( 'Error: Unable to create order. Please try again.', 'woocommerce' ) ); - } - - // Allow plugins to add order item meta - do_action( 'woocommerce_add_order_item_meta', $item_id, $values, $cart_item_key ); - } - - // Store fees - foreach ( WC()->cart->get_fees() as $fee_key => $fee ) { - $item_id = $order->add_fee( $fee ); - - if ( ! $item_id ) { - throw new Exception( __( 'Error: Unable to create order. Please try again.', 'woocommerce' ) ); - } - - // Allow plugins to add order item meta to fees - do_action( 'woocommerce_add_order_fee_meta', $order_id, $item_id, $fee, $fee_key ); - } - - // Store shipping for all packages - foreach ( WC()->shipping->get_packages() as $package_key => $package ) { - if ( isset( $package['rates'][ $this->shipping_methods[ $package_key ] ] ) ) { - $item_id = $order->add_shipping( $package['rates'][ $this->shipping_methods[ $package_key ] ] ); - - if ( ! $item_id ) { - throw new Exception( __( 'Error: Unable to create order. Please try again.', 'woocommerce' ) ); - } - - // Allows plugins to add order item meta to shipping - do_action( 'woocommerce_add_shipping_order_item', $order_id, $item_id, $package_key ); - } - } - - // Store tax rows - foreach ( array_keys( WC()->cart->taxes + WC()->cart->shipping_taxes ) as $tax_rate_id ) { - if ( ! $order->add_tax( $tax_rate_id, WC()->cart->get_tax_amount( $tax_rate_id ), WC()->cart->get_shipping_tax_amount( $tax_rate_id ) ) ) { - throw new Exception( __( 'Error: Unable to create order. Please try again.', 'woocommerce' ) ); - } - } - - // Store coupons - foreach ( WC()->cart->get_coupons() as $code => $coupon ) { - if ( ! $order->add_coupon( $code, WC()->cart->get_coupon_discount_amount( $code ) ) ) { - throw new Exception( __( 'Error: Unable to create order. Please try again.', 'woocommerce' ) ); - } - } - - // Billing address - $billing_address = array( - 'first_name' => $this->get_posted_address_data( 'first_name' ), - 'last_name' => $this->get_posted_address_data( 'last_name' ), - 'company' => $this->get_posted_address_data( 'company' ), - 'email' => $this->get_posted_address_data( 'email' ), - 'phone' => $this->get_posted_address_data( 'phone' ), - 'address_1' => $this->get_posted_address_data( 'address_1' ), - 'address_2' => $this->get_posted_address_data( 'address_2' ), - 'city' => $this->get_posted_address_data( 'city' ), - 'state' => $this->get_posted_address_data( 'state' ), - 'postcode' => $this->get_posted_address_data( 'postcode' ), - 'country' => $this->get_posted_address_data( 'country' ) - ); - - $shipping_address = array( - 'first_name' => $this->get_posted_address_data( 'first_name', 'shipping' ), - 'last_name' => $this->get_posted_address_data( 'last_name', 'shipping' ), - 'company' => $this->get_posted_address_data( 'company', 'shipping' ), - 'address_1' => $this->get_posted_address_data( 'address_1', 'shipping' ), - 'address_2' => $this->get_posted_address_data( 'address_2', 'shipping' ), - 'city' => $this->get_posted_address_data( 'city', 'shipping' ), - 'state' => $this->get_posted_address_data( 'state', 'shipping' ), - 'postcode' => $this->get_posted_address_data( 'postcode', 'shipping' ), - 'country' => $this->get_posted_address_data( 'country', 'shipping' ), - ); - - $order->set_address( $billing_address, 'billing' ); - $order->set_address( $shipping_address, 'shipping' ); - $order->set_payment_method( $this->payment_method ); - $order->set_total( WC()->cart->shipping_total, 'shipping' ); - $order->set_total( WC()->cart->get_order_discount_total(), 'order_discount' ); - $order->set_total( WC()->cart->get_cart_discount_total(), 'cart_discount' ); - $order->set_total( WC()->cart->tax_total, 'tax' ); - $order->set_total( WC()->cart->shipping_tax_total, 'shipping_tax' ); + $order->set_created_via( 'checkout' ); + $order->set_cart_hash( $cart_hash ); + $order->set_customer_id( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) ); + $order->set_currency( get_woocommerce_currency() ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->set_customer_ip_address( WC_Geolocation::get_ip_address() ); + $order->set_customer_user_agent( wc_get_user_agent() ); + $order->set_customer_note( isset( $data['order_comments'] ) ? $data['order_comments'] : '' ); + $order->set_payment_method( isset( $available_gateways[ $data['payment_method'] ] ) ? $available_gateways[ $data['payment_method'] ] : $data['payment_method'] ); + $order->set_shipping_total( WC()->cart->shipping_total ); + $order->set_discount_total( WC()->cart->get_cart_discount_total() ); + $order->set_discount_tax( WC()->cart->get_cart_discount_tax_total() ); + $order->set_cart_tax( WC()->cart->tax_total ); + $order->set_shipping_tax( WC()->cart->shipping_tax_total ); $order->set_total( WC()->cart->total ); + $this->create_order_line_items( $order, WC()->cart ); + $this->create_order_fee_lines( $order, WC()->cart ); + $this->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping->get_packages() ); + $this->create_order_tax_lines( $order, WC()->cart ); + $this->create_order_coupon_lines( $order, WC()->cart ); - // Update user meta - if ( $this->customer_id ) { - if ( apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) { - foreach( $billing_address as $key => $value ) { - update_user_meta( $this->customer_id, 'billing_' . $key, $value ); - } - foreach( $shipping_address as $key => $value ) { - update_user_meta( $this->customer_id, 'shipping_' . $key, $value ); - } - } - do_action( 'woocommerce_checkout_update_user_meta', $this->customer_id, $this->posted ); - } + /** + * Action hook to adjust order before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_checkout_create_order', $order, $data ); - // Let plugins add meta - do_action( 'woocommerce_checkout_update_order_meta', $order_id, $this->posted ); + // Save the order. + $order_id = $order->save(); - // If we got here, the order was created without problems! - $wpdb->query( 'COMMIT' ); + do_action( 'woocommerce_checkout_update_order_meta', $order_id, $data ); + return $order_id; } catch ( Exception $e ) { - // There was an error adding order data! - $wpdb->query( 'ROLLBACK' ); return new WP_Error( 'checkout-error', $e->getMessage() ); } - - return $order_id; } /** - * Process the checkout after the confirm order button is pressed + * Add line items to the order. * - * @access public - * @return void + * @param WC_Order $order + * @param WC_Cart $cart */ - public function process_checkout() { - wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-process_checkout' ); + public function create_order_line_items( &$order, $cart ) { + foreach ( $cart->get_cart() as $cart_item_key => $values ) { + /** + * Filter hook to get inital item object. + * @since 3.1.0 + */ + $item = apply_filters( 'woocommerce_checkout_create_order_line_item_object', new WC_Order_Item_Product(), $cart_item_key, $values, $order ); + $product = $values['data']; + $item->legacy_values = $values; // @deprecated For legacy actions. + $item->legacy_cart_item_key = $cart_item_key; // @deprecated For legacy actions. + $item->set_props( array( + 'quantity' => $values['quantity'], + 'variation' => $values['variation'], + 'subtotal' => $values['line_subtotal'], + 'total' => $values['line_total'], + 'subtotal_tax' => $values['line_subtotal_tax'], + 'total_tax' => $values['line_tax'], + 'taxes' => $values['line_tax_data'], + ) ); + if ( $product ) { + $item->set_props( array( + 'name' => $product->get_name(), + 'tax_class' => $product->get_tax_class(), + 'product_id' => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(), + 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0, + ) ); + } + $item->set_backorder_meta(); - if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) - define( 'WOOCOMMERCE_CHECKOUT', true ); + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_checkout_create_order_line_item', $item, $cart_item_key, $values, $order ); - // Prevent timeout - @set_time_limit(0); - - do_action( 'woocommerce_before_checkout_process' ); - - if ( sizeof( WC()->cart->get_cart() ) == 0 ) - wc_add_notice( sprintf( __( 'Sorry, your session has expired. Return to homepage', 'woocommerce' ), home_url() ), 'error' ); - - do_action( 'woocommerce_checkout_process' ); - - // Checkout fields (not defined in checkout_fields) - $this->posted['terms'] = isset( $_POST['terms'] ) ? 1 : 0; - $this->posted['createaccount'] = isset( $_POST['createaccount'] ) ? 1 : 0; - $this->posted['payment_method'] = isset( $_POST['payment_method'] ) ? stripslashes( $_POST['payment_method'] ) : ''; - $this->posted['shipping_method'] = isset( $_POST['shipping_method'] ) ? $_POST['shipping_method'] : ''; - $this->posted['ship_to_different_address'] = isset( $_POST['ship_to_different_address'] ) ? true : false; - - if ( isset( $_POST['shiptobilling'] ) ) { - _deprecated_argument( 'WC_Checkout::process_checkout()', '2.1', 'The "shiptobilling" field is deprecated. THe template files are out of date' ); - - $this->posted['ship_to_different_address'] = $_POST['shiptobilling'] ? false : true; + // Add item to order and save. + $order->add_item( $item ); } + } - // Ship to billing only option - if ( WC()->cart->ship_to_billing_address_only() ) - $this->posted['ship_to_different_address'] = false; + /** + * Add fees to the order. + * + * @param WC_Order $order + * @param WC_Cart $cart + */ + public function create_order_fee_lines( &$order, $cart ) { + foreach ( $cart->get_fees() as $fee_key => $fee ) { + $item = new WC_Order_Item_Fee(); + $item->legacy_fee = $fee; // @deprecated For legacy actions. + $item->legacy_fee_key = $fee_key; // @deprecated For legacy actions. + $item->set_props( array( + 'name' => $fee->name, + 'tax_class' => $fee->taxable ? $fee->tax_class : 0, + 'total' => $fee->amount, + 'total_tax' => $fee->tax, + 'taxes' => array( + 'total' => $fee->tax_data, + ), + ) ); - // Update customer shipping and payment method to posted method - $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_checkout_create_order_fee_item', $item, $fee_key, $fee, $order ); - if ( isset( $this->posted['shipping_method'] ) && is_array( $this->posted['shipping_method'] ) ) { - foreach ( $this->posted['shipping_method'] as $i => $value ) { - $chosen_shipping_methods[ $i ] = wc_clean( $value ); + // Add item to order and save. + $order->add_item( $item ); + } + } + + /** + * Add shipping lines to the order. + * + * @param WC_Order $order + * @param array $chosen_shipping_methods + * @param array $packages + */ + public function create_order_shipping_lines( &$order, $chosen_shipping_methods, $packages ) { + foreach ( $packages as $package_key => $package ) { + if ( isset( $chosen_shipping_methods[ $package_key ], $package['rates'][ $chosen_shipping_methods[ $package_key ] ] ) ) { + /** @var WC_Shipping_Rate $shipping_rate */ + $shipping_rate = $package['rates'][ $chosen_shipping_methods[ $package_key ] ]; + $item = new WC_Order_Item_Shipping(); + $item->legacy_package_key = $package_key; // @deprecated For legacy actions. + $item->set_props( array( + 'method_title' => $shipping_rate->label, + 'method_id' => $shipping_rate->id, + 'total' => wc_format_decimal( $shipping_rate->cost ), + 'taxes' => array( + 'total' => $shipping_rate->taxes, + ), + ) ); + + foreach ( $shipping_rate->get_meta_data() as $key => $value ) { + $item->add_meta_data( $key, $value, true ); + } + + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_checkout_create_order_shipping_item', $item, $package_key, $package, $order ); + + // Add item to order and save. + $order->add_item( $item ); } } + } - WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); - WC()->session->set( 'chosen_payment_method', $this->posted['payment_method'] ); + /** + * Add tax lines to the order. + * + * @param WC_Order $order + * @param WC_Cart $cart + */ + public function create_order_tax_lines( &$order, $cart ) { + foreach ( array_keys( $cart->taxes + $cart->shipping_taxes ) as $tax_rate_id ) { + if ( $tax_rate_id && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) { + $item = new WC_Order_Item_Tax(); + $item->set_props( array( + 'rate_id' => $tax_rate_id, + 'tax_total' => $cart->get_tax_amount( $tax_rate_id ), + 'shipping_tax_total' => $cart->get_shipping_tax_amount( $tax_rate_id ), + 'rate_code' => WC_Tax::get_rate_code( $tax_rate_id ), + 'label' => WC_Tax::get_rate_label( $tax_rate_id ), + 'compound' => WC_Tax::is_compound( $tax_rate_id ), + ) ); - // Note if we skip shipping - $skipped_shipping = false; + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_checkout_create_order_tax_item', $item, $tax_rate_id, $order ); - // Get posted checkout_fields and do validation - foreach ( $this->checkout_fields as $fieldset_key => $fieldset ) { + // Add item to order and save. + $order->add_item( $item ); + } + } + } - // Skip shipping if not needed - if ( $fieldset_key == 'shipping' && ( $this->posted['ship_to_different_address'] == false || ! WC()->cart->needs_shipping() ) ) { - $skipped_shipping = true; + /** + * Add coupon lines to the order. + * + * @param WC_Order $order + * @param WC_Cart $cart + */ + public function create_order_coupon_lines( &$order, $cart ) { + foreach ( $cart->get_coupons() as $code => $coupon ) { + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $code, + 'discount' => $cart->get_coupon_discount_amount( $code ), + 'discount_tax' => $cart->get_coupon_discount_tax_amount( $code ), + ) ); + + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_checkout_create_order_coupon_item', $item, $code, $coupon, $order ); + + // Add item to order and save. + $order->add_item( $item ); + } + } + + /** + * See if a fieldset should be skipped. + * + * @since 3.0.0 + * + * @param string $fieldset_key + * @param array $data + * + * @return bool + */ + protected function maybe_skip_fieldset( $fieldset_key, $data ) { + if ( 'shipping' === $fieldset_key && ( ! $data['ship_to_different_address'] || ! WC()->cart->needs_shipping_address() ) ) { + return true; + } + if ( 'account' === $fieldset_key && ( is_user_logged_in() || ( ! $this->is_registration_required() && empty( $data['createaccount'] ) ) ) ) { + return true; + } + return false; + } + + /** + * Get posted data from the checkout form. + * + * @since 3.1.0 + * @return array of data. + */ + public function get_posted_data() { + $skipped = array(); + $data = array( + 'terms' => (int) isset( $_POST['terms'] ), + 'createaccount' => (int) ! empty( $_POST['createaccount'] ), + 'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : '', + 'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( $_POST['shipping_method'] ) : '', + 'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(), + 'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ), + ); + foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) { + if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) { + $skipped[] = $fieldset_key; continue; } - - // Ship account if not needed - if ( $fieldset_key == 'account' && ( is_user_logged_in() || ( $this->must_create_account == false && empty( $this->posted['createaccount'] ) ) ) ) { - continue; - } - foreach ( $fieldset as $key => $field ) { + $type = sanitize_title( isset( $field['type'] ) ? $field['type'] : 'text' ); - if ( ! isset( $field['type'] ) ) - $field['type'] = 'text'; - - // Get Value - switch ( $field['type'] ) { - case "checkbox" : - $this->posted[ $key ] = isset( $_POST[ $key ] ) ? 1 : 0; - break; - case "multiselect" : - $this->posted[ $key ] = isset( $_POST[ $key ] ) ? implode( ', ', array_map( 'wc_clean', $_POST[ $key ] ) ) : ''; - break; - case "textarea" : - $this->posted[ $key ] = isset( $_POST[ $key ] ) ? wp_strip_all_tags( wp_check_invalid_utf8( stripslashes( $_POST[ $key ] ) ) ) : ''; - break; + switch ( $type ) { + case 'checkbox' : + $value = (int) isset( $_POST[ $key ] ); + break; + case 'multiselect' : + $value = isset( $_POST[ $key ] ) ? implode( ', ', wc_clean( $_POST[ $key ] ) ) : ''; + break; + case 'textarea' : + $value = isset( $_POST[ $key ] ) ? wc_sanitize_textarea( $_POST[ $key ] ) : ''; + break; default : - $this->posted[ $key ] = isset( $_POST[ $key ] ) ? ( is_array( $_POST[ $key ] ) ? array_map( 'wc_clean', $_POST[ $key ] ) : wc_clean( $_POST[ $key ] ) ) : ''; + $value = isset( $_POST[ $key ] ) ? wc_clean( $_POST[ $key ] ) : ''; + break; + } + + $data[ $key ] = apply_filters( 'woocommerce_process_checkout_' . $type . '_field', apply_filters( 'woocommerce_process_checkout_field_' . $key, $value ) ); + } + } + + if ( in_array( 'shipping', $skipped ) && ( WC()->cart->needs_shipping_address() || wc_ship_to_billing_address_only() ) ) { + foreach ( $this->get_checkout_fields( 'shipping' ) as $key => $field ) { + $data[ $key ] = isset( $data[ 'billing_' . substr( $key, 9 ) ] ) ? $data[ 'billing_' . substr( $key, 9 ) ] : ''; + } + } + + // BW compatibility. + $this->legacy_posted_data = $data; + + return $data; + } + + /** + * Validates the posted checkout data based on field properties. + * + * @since 3.0.0 + * @param array $data An array of posted data. + * @param WP_Error $errors + */ + protected function validate_posted_data( &$data, &$errors ) { + foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) { + if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) { + continue; + } + foreach ( $fieldset as $key => $field ) { + if ( ! isset( $data[ $key ] ) ) { + continue; + } + $required = ! empty( $field['required'] ); + $format = array_filter( isset( $field['validate'] ) ? (array) $field['validate'] : array() ); + $field_label = isset( $field['label'] ) ? $field['label'] : ''; + + switch ( $fieldset_key ) { + case 'shipping' : + /* translators: %s: field name */ + $field_label = sprintf( __( 'Shipping %s', 'woocommerce' ), $field_label ); + break; + case 'billing' : + /* translators: %s: field name */ + $field_label = sprintf( __( 'Billing %s', 'woocommerce' ), $field_label ); break; } - // Hooks to allow modification of value - $this->posted[ $key ] = apply_filters( 'woocommerce_process_checkout_' . sanitize_title( $field['type'] ) . '_field', $this->posted[ $key ] ); - $this->posted[ $key ] = apply_filters( 'woocommerce_process_checkout_field_' . $key, $this->posted[ $key ] ); + if ( in_array( 'postcode', $format ) ) { + $country = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}(); + $data[ $key ] = wc_format_postcode( $data[ $key ], $country ); - // Validation: Required fields - if ( isset( $field['required'] ) && $field['required'] && empty( $this->posted[ $key ] ) ) - wc_add_notice( '' . $field['label'] . ' ' . __( 'is a required field.', 'woocommerce' ), 'error' ); + if ( '' !== $data[ $key ] && ! WC_Validation::is_postcode( $data[ $key ], $country ) ) { + $errors->add( 'validation', __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ) ); + } + } - if ( ! empty( $this->posted[ $key ] ) ) { + if ( in_array( 'phone', $format ) ) { + $data[ $key ] = wc_format_phone_number( $data[ $key ] ); - // Validation rules - if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) { - foreach ( $field['validate'] as $rule ) { - switch ( $rule ) { - case 'postcode' : - $this->posted[ $key ] = strtoupper( str_replace( ' ', '', $this->posted[ $key ] ) ); + if ( '' !== $data[ $key ] && ! WC_Validation::is_phone( $data[ $key ] ) ) { + /* translators: %s: phone number */ + $errors->add( 'validation', sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '' . esc_html( $field_label ) . '' ) ); + } + } - if ( ! WC_Validation::is_postcode( $this->posted[ $key ], $_POST[ $fieldset_key . '_country' ] ) ) : - wc_add_notice( __( 'Please enter a valid postcode/ZIP.', 'woocommerce' ), 'error' ); - else : - $this->posted[ $key ] = wc_format_postcode( $this->posted[ $key ], $_POST[ $fieldset_key . '_country' ] ); - endif; - break; - case 'phone' : - $this->posted[ $key ] = wc_format_phone_number( $this->posted[ $key ] ); + if ( in_array( 'email', $format ) && '' !== $data[ $key ] ) { + $data[ $key ] = sanitize_email( $data[ $key ] ); - if ( ! WC_Validation::is_phone( $this->posted[ $key ] ) ) - wc_add_notice( '' . $field['label'] . ' ' . __( 'is not a valid phone number.', 'woocommerce' ), 'error' ); - break; - case 'email' : - $this->posted[ $key ] = strtolower( $this->posted[ $key ] ); + if ( ! is_email( $data[ $key ] ) ) { + /* translators: %s: email address */ + $errors->add( 'validation', sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '' . $field_label . '' ) ); + continue; + } + } - if ( ! is_email( $this->posted[ $key ] ) ) - wc_add_notice( '' . $field['label'] . ' ' . __( 'is not a valid email address.', 'woocommerce' ), 'error' ); - break; - case 'state' : - // Get valid states - $valid_states = WC()->countries->get_states( $_POST[ $fieldset_key . '_country' ] ); - if ( $valid_states ) - $valid_state_values = array_flip( array_map( 'strtolower', $valid_states ) ); + if ( '' !== $data[ $key ] && in_array( 'state', $format ) ) { + $country = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}(); + $valid_states = WC()->countries->get_states( $country ); - // Convert value to key if set - if ( isset( $valid_state_values[ strtolower( $this->posted[ $key ] ) ] ) ) - $this->posted[ $key ] = $valid_state_values[ strtolower( $this->posted[ $key ] ) ]; + if ( ! empty( $valid_states ) && is_array( $valid_states ) && sizeof( $valid_states ) > 0 ) { + $valid_state_values = array_flip( array_map( 'wc_strtoupper', $valid_states ) ); + $data[ $key ] = wc_strtoupper( $data[ $key ] ); - // Only validate if the country has specific state options - if ( $valid_states && sizeof( $valid_states ) > 0 ) - if ( ! in_array( $this->posted[ $key ], array_keys( $valid_states ) ) ) - wc_add_notice( '' . $field['label'] . ' ' . __( 'is not valid. Please enter one of the following:', 'woocommerce' ) . ' ' . implode( ', ', $valid_states ), 'error' ); - break; - } + if ( isset( $valid_state_values[ $data[ $key ] ] ) ) { + // With this part we consider state value to be valid as well, convert it to the state key for the valid_states check below. + $data[ $key ] = $valid_state_values[ $data[ $key ] ]; + } + + if ( ! in_array( $data[ $key ], array_keys( $valid_states ) ) ) { + /* 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' ), '' . $field_label . '', implode( ', ', $valid_states ) ) ); } } } + + if ( $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' ), '' . $field_label . '' ), $field_label ) ); + } } } + } - // Update customer location to posted location so we can correctly check available shipping methods - if ( isset( $this->posted['billing_country'] ) ) - WC()->customer->set_country( $this->posted['billing_country'] ); - if ( isset( $this->posted['billing_state'] ) ) - WC()->customer->set_state( $this->posted['billing_state'] ); - if ( isset( $this->posted['billing_postcode'] ) ) - WC()->customer->set_postcode( $this->posted['billing_postcode'] ); - - // Shipping Information - if ( ! $skipped_shipping ) { - - // Update customer location to posted location so we can correctly check available shipping methods - if ( isset( $this->posted['shipping_country'] ) ) - WC()->customer->set_shipping_country( $this->posted['shipping_country'] ); - if ( isset( $this->posted['shipping_state'] ) ) - WC()->customer->set_shipping_state( $this->posted['shipping_state'] ); - if ( isset( $this->posted['shipping_postcode'] ) ) - WC()->customer->set_shipping_postcode( $this->posted['shipping_postcode'] ); - - } else { - - // Update customer location to posted location so we can correctly check available shipping methods - if ( isset( $this->posted['billing_country'] ) ) - WC()->customer->set_shipping_country( $this->posted['billing_country'] ); - if ( isset( $this->posted['billing_state'] ) ) - WC()->customer->set_shipping_state( $this->posted['billing_state'] ); - if ( isset( $this->posted['billing_postcode'] ) ) - WC()->customer->set_shipping_postcode( $this->posted['billing_postcode'] ); + /** + * Validates that the checkout has enough info to proceed. + * + * @since 3.0.0 + * @param array $data An array of posted data. + * @param WP_Error $errors + */ + protected function validate_checkout( &$data, &$errors ) { + $this->validate_posted_data( $data, $errors ); + $this->check_cart_items(); + if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && apply_filters( 'woocommerce_checkout_show_terms', wc_get_page_id( 'terms' ) > 0 ) ) { + $errors->add( 'terms', __( 'You must accept our Terms & Conditions.', 'woocommerce' ) ); } - // Update cart totals now we have customer address - WC()->cart->calculate_totals(); - - // Terms - if ( ! isset( $_POST['woocommerce_checkout_update_totals'] ) && empty( $this->posted['terms'] ) && wc_get_page_id( 'terms' ) > 0 ) - wc_add_notice( __( 'You must accept our Terms & Conditions.', 'woocommerce' ), 'error' ); - if ( WC()->cart->needs_shipping() ) { + $shipping_country = WC()->customer->get_shipping_country(); - if ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ) ) ) - wc_add_notice( sprintf( __( 'Unfortunately we do not ship %s. Please enter an alternative shipping address.', 'woocommerce' ), WC()->countries->shipping_to_prefix() . ' ' . WC()->customer->get_shipping_country() ), 'error' ); + if ( empty( $shipping_country ) ) { + $errors->add( 'shipping', __( 'Please enter an address to continue.', 'woocommerce' ) ); + } elseif ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ) ) ) { + $errors->add( 'shipping', sprintf( __( 'Unfortunately we do not ship %s. Please enter an alternative shipping address.', 'woocommerce' ), WC()->countries->shipping_to_prefix() . ' ' . WC()->customer->get_shipping_country() ) ); + } else { + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); - // Validate Shipping Methods - $packages = WC()->shipping->get_packages(); - $this->shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); - - foreach ( $packages as $i => $package ) { - if ( ! isset( $package['rates'][ $this->shipping_methods[ $i ] ] ) ) { - wc_add_notice( __( 'Invalid shipping method.', 'woocommerce' ), 'error' ); - $this->shipping_methods[ $i ] = ''; + foreach ( WC()->shipping->get_packages() as $i => $package ) { + if ( ! isset( $chosen_shipping_methods[ $i ], $package['rates'][ $chosen_shipping_methods[ $i ] ] ) ) { + $errors->add( 'shipping', __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'woocommerce' ) ); + } } } } if ( WC()->cart->needs_payment() ) { - - // Payment Method $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - if ( ! isset( $available_gateways[ $this->posted['payment_method'] ] ) ) { - $this->payment_method = ''; - wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); + if ( ! isset( $available_gateways[ $data['payment_method'] ] ) ) { + $errors->add( 'payment', __( 'Invalid payment method.', 'woocommerce' ) ); } else { - $this->payment_method = $available_gateways[ $this->posted['payment_method'] ]; - $this->payment_method->validate_fields(); + $available_gateways[ $data['payment_method'] ]->validate_fields(); } } - // Action after validation - do_action( 'woocommerce_after_checkout_validation', $this->posted ); + do_action( 'woocommerce_after_checkout_validation', $data, $errors ); + } - if ( ! isset( $_POST['woocommerce_checkout_update_totals'] ) && wc_notice_count( 'error' ) == 0 ) { + /** + * Set address field for customer. + * + * @since 3.0.7 + * @param $field string to update + * @param $key + * @param $data array of data to get the value from + */ + protected function set_customer_address_fields( $field, $key, $data ) { + if ( isset( $data[ "billing_{$field}" ] ) ) { + WC()->customer->{"set_billing_{$field}"}( $data[ "billing_{$field}" ] ); + WC()->customer->{"set_shipping_{$field}"}( $data[ "billing_{$field}" ] ); + } + if ( isset( $data[ "shipping_{$field}" ] ) ) { + WC()->customer->{"set_shipping_{$field}"}( $data[ "shipping_{$field}" ] ); + } + } - try { + /** + * Update customer and session data from the posted checkout data. + * + * @since 3.0.0 + * @param array $data + */ + protected function update_session( $data ) { + // Update both shipping and billing to the passed billing address first if set. + $address_fields = array( + 'address_1', + 'address_2', + 'city', + 'postcode', + 'state', + 'country', + ); - // Customer accounts - $this->customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ); + array_walk( $address_fields, array( $this, 'set_customer_address_fields' ), $data ); + WC()->customer->save(); - if ( ! is_user_logged_in() && ( $this->must_create_account || ! empty( $this->posted['createaccount'] ) ) ) { + // Update customer shipping and payment method to posted method + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); - $username = ! empty( $this->posted['account_username'] ) ? $this->posted['account_username'] : ''; - $password = ! empty( $this->posted['account_password'] ) ? $this->posted['account_password'] : ''; - $new_customer = wc_create_new_customer( $this->posted['billing_email'], $username, $password ); - - if ( is_wp_error( $new_customer ) ) - throw new Exception( $new_customer->get_error_message() ); - - $this->customer_id = $new_customer; - - wc_set_customer_auth_cookie( $this->customer_id ); - - // As we are now logged in, checkout will need to refresh to show logged in data - WC()->session->set( 'reload_checkout', true ); - - // Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering - WC()->cart->calculate_totals(); - - // Add customer info from other billing fields - if ( $this->posted['billing_first_name'] && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) { - $userdata = array( - 'ID' => $this->customer_id, - 'first_name' => $this->posted['billing_first_name'] ? $this->posted['billing_first_name'] : '', - 'last_name' => $this->posted['billing_last_name'] ? $this->posted['billing_last_name'] : '', - 'display_name' => $this->posted['billing_first_name'] ? $this->posted['billing_first_name'] : '' - ); - wp_update_user( apply_filters( 'woocommerce_checkout_customer_userdata', $userdata, $this ) ); - } - } - - // Do a final stock check at this point - $this->check_cart_items(); - - // Abort if errors are present - if ( wc_notice_count( 'error' ) > 0 ) - throw new Exception(); - - $order_id = $this->create_order(); - - if ( is_wp_error( $order_id ) ) { - throw new Exception( $order_id->get_error_message() ); - } - - do_action( 'woocommerce_checkout_order_processed', $order_id, $this->posted ); - - // Process payment - if ( WC()->cart->needs_payment() ) { - - // Store Order ID in session so it can be re-used after payment failure - WC()->session->order_awaiting_payment = $order_id; - - // Process Payment - $result = $available_gateways[ $this->posted['payment_method'] ]->process_payment( $order_id ); - - // Redirect to success/confirmation/payment page - if ( $result['result'] == 'success' ) { - - $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); - - if ( is_ajax() ) { - echo '' . json_encode( $result ) . ''; - exit; - } else { - wp_redirect( $result['redirect'] ); - exit; - } - - } - - } else { - - if ( empty( $order ) ) - $order = get_order( $order_id ); - - // No payment was required for order - $order->payment_complete(); - - // Empty the Cart - WC()->cart->empty_cart(); - - // Get redirect - $return_url = $order->get_checkout_order_received_url(); - - // Redirect to success/confirmation/payment page - if ( is_ajax() ) { - echo '' . json_encode( - array( - 'result' => 'success', - 'redirect' => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $return_url, $order ) - ) - ) . ''; - exit; - } else { - wp_safe_redirect( - apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $return_url, $order ) - ); - exit; - } - - } - - } catch ( Exception $e ) { - if ( ! empty( $e ) ) { - wc_add_notice( $e->getMessage(), 'error' ); - } + if ( is_array( $data['shipping_method'] ) ) { + foreach ( $data['shipping_method'] as $i => $value ) { + $chosen_shipping_methods[ $i ] = $value; } + } - } // endif + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + WC()->session->set( 'chosen_payment_method', $data['payment_method'] ); + + // Update cart totals now we have customer address. + WC()->cart->calculate_totals(); + } + + + /** + * Process an order that does require payment. + * + * @since 3.0.0 + * @param int $order_id + * @param string $payment_method + */ + protected function process_order_payment( $order_id, $payment_method ) { + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + + if ( ! isset( $available_gateways[ $payment_method ] ) ) { + return; + } + + // Store Order ID in session so it can be re-used after payment failure + WC()->session->set( 'order_awaiting_payment', $order_id ); + + // Process Payment + $result = $available_gateways[ $payment_method ]->process_payment( $order_id ); + + // Redirect to success/confirmation/payment page + if ( isset( $result['result'] ) && 'success' === $result['result'] ) { + $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); + + if ( is_ajax() ) { + wp_send_json( $result ); + } else { + wp_redirect( $result['redirect'] ); + exit; + } + } + } + + /** + * Process an order that doesn't require payment. + * + * @since 3.0.0 + * @param int $order_id + */ + protected function process_order_without_payment( $order_id ) { + $order = wc_get_order( $order_id ); + $order->payment_complete(); + wc_empty_cart(); - // If we reached this point then there were errors if ( is_ajax() ) { - - // only print notices if not reloading the checkout, otherwise they're lost in the page reload - if ( ! isset( WC()->session->reload_checkout ) ) { - - ob_start(); - wc_print_notices(); - $messages = ob_get_clean(); - } - - - echo '' . json_encode( - array( - 'result' => 'failure', - 'messages' => isset( $messages ) ? $messages : '', - 'refresh' => isset( WC()->session->refresh_totals ) ? 'true' : 'false', - 'reload' => isset( WC()->session->reload_checkout ) ? 'true' : 'false' - ) - ) . ''; - - unset( WC()->session->refresh_totals, WC()->session->reload_checkout ); + wp_send_json( array( + 'result' => 'success', + 'redirect' => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order ), + ) ); + } else { + wp_safe_redirect( + apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order ) + ); exit; } } + /** + * Create a new customer account if needed. + * @param array $data + * @throws Exception + */ + protected function process_customer( $data ) { + $customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ); + + if ( ! is_user_logged_in() && ( $this->is_registration_required() || ! empty( $data['createaccount'] ) ) ) { + $username = ! empty( $data['account_username'] ) ? $data['account_username'] : ''; + $password = ! empty( $data['account_password'] ) ? $data['account_password'] : ''; + $customer_id = wc_create_new_customer( $data['billing_email'], $username, $password ); + + if ( is_wp_error( $customer_id ) ) { + throw new Exception( $customer_id->get_error_message() ); + } + + wp_set_current_user( $customer_id ); + wc_set_customer_auth_cookie( $customer_id ); + + // As we are now logged in, checkout will need to refresh to show logged in data + WC()->session->set( 'reload_checkout', true ); + + // Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering + WC()->cart->calculate_totals(); + } + + // On multisite, ensure user exists on current site, if not add them before allowing login. + if ( $customer_id && is_multisite() && is_user_logged_in() && ! is_user_member_of_blog() ) { + add_user_to_blog( get_current_blog_id(), $customer_id, 'customer' ); + } + + // Add customer info from other fields. + if ( $customer_id && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) { + $customer = new WC_Customer( $customer_id ); + + if ( ! empty( $data['billing_first_name'] ) ) { + $customer->set_first_name( $data['billing_first_name'] ); + } + + if ( ! empty( $data['billing_last_name'] ) ) { + $customer->set_last_name( $data['billing_last_name'] ); + } + + // If the display name is an email, update to the user's full name. + if ( is_email( $customer->get_display_name() ) ) { + $customer->set_display_name( $data['billing_first_name'] . ' ' . $data['billing_last_name'] ); + } + + foreach ( $data as $key => $value ) { + // Use setters where available. + if ( is_callable( array( $customer, "set_{$key}" ) ) ) { + $customer->{"set_{$key}"}( $value ); + + // Store custom fields prefixed with wither shipping_ or billing_. + } elseif ( 0 === stripos( $key, 'billing_' ) || 0 === stripos( $key, 'shipping_' ) ) { + $customer->update_meta_data( $key, $value ); + } + } + + /** + * Action hook to adjust customer before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_checkout_update_customer', $customer, $data ); + + $customer->save(); + } + + do_action( 'woocommerce_checkout_update_user_meta', $customer_id, $data ); + } + + /** + * If checkout failed during an AJAX call, send failure response. + */ + protected function send_ajax_failure_response() { + if ( is_ajax() ) { + // only print notices if not reloading the checkout, otherwise they're lost in the page reload + if ( ! isset( WC()->session->reload_checkout ) ) { + ob_start(); + wc_print_notices(); + $messages = ob_get_clean(); + } + + $response = array( + 'result' => 'failure', + 'messages' => isset( $messages ) ? $messages : '', + 'refresh' => isset( WC()->session->refresh_totals ), + 'reload' => isset( WC()->session->reload_checkout ), + ); + + unset( WC()->session->refresh_totals, WC()->session->reload_checkout ); + + wp_send_json( $response ); + } + } + + /** + * Process the checkout after the confirm order button is pressed. + */ + public function process_checkout() { + try { + if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-process_checkout' ) ) { + WC()->session->set( 'refresh_totals', true ); + throw new Exception( __( 'We were unable to process your order, please try again.', 'woocommerce' ) ); + } + + wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); + wc_set_time_limit( 0 ); + + do_action( 'woocommerce_before_checkout_process' ); + + if ( WC()->cart->is_empty() ) { + throw new Exception( sprintf( __( 'Sorry, your session has expired. Return to shop', 'woocommerce' ), esc_url( wc_get_page_permalink( 'shop' ) ) ) ); + } + + do_action( 'woocommerce_checkout_process' ); + + $errors = new WP_Error(); + $posted_data = $this->get_posted_data(); + + // Update session for customer and totals. + $this->update_session( $posted_data ); + + // Validate posted data and cart items before proceeding. + $this->validate_checkout( $posted_data, $errors ); + + foreach ( $errors->get_error_messages() as $message ) { + wc_add_notice( $message, 'error' ); + } + + if ( empty( $posted_data['woocommerce_checkout_update_totals'] ) && 0 === wc_notice_count( 'error' ) ) { + $this->process_customer( $posted_data ); + $order_id = $this->create_order( $posted_data ); + $order = wc_get_order( $order_id ); + + if ( is_wp_error( $order_id ) ) { + throw new Exception( $order_id->get_error_message() ); + } + + do_action( 'woocommerce_checkout_order_processed', $order_id, $posted_data, $order ); + + if ( WC()->cart->needs_payment() ) { + $this->process_order_payment( $order_id, $posted_data['payment_method'] ); + } else { + $this->process_order_without_payment( $order_id ); + } + } + } catch ( Exception $e ) { + wc_add_notice( $e->getMessage(), 'error' ); + } + $this->send_ajax_failure_response(); + } + /** * Get a posted address field after sanitization and validation. + * * @param string $key * @param string $type billing for shipping * @return string */ public function get_posted_address_data( $key, $type = 'billing' ) { - if ( 'billing' === $type || false === $this->posted['ship_to_different_address'] ) { - $return = isset( $this->posted[ 'billing_' . $key ] ) ? $this->posted[ 'billing_' . $key ] : ''; + if ( 'billing' === $type || false === $this->legacy_posted_data['ship_to_different_address'] ) { + $return = isset( $this->legacy_posted_data[ 'billing_' . $key ] ) ? $this->legacy_posted_data[ 'billing_' . $key ] : ''; } else { - $return = isset( $this->posted[ 'shipping_' . $key ] ) ? $this->posted[ 'shipping_' . $key ] : ''; - } - - // Use logged in user's billing email if neccessary - if ( 'email' === $key && empty( $return ) && is_user_logged_in() ) { - $current_user = wp_get_current_user(); - $return = $current_user->user_email; + $return = isset( $this->legacy_posted_data[ 'shipping_' . $key ] ) ? $this->legacy_posted_data[ 'shipping_' . $key ] : ''; } return $return; } /** - * Gets the value either from the posted data, or from the users meta data + * Gets the value either from the posted data, or from the users meta data. * - * @access public * @param string $input - * @return string|null + * @return string */ public function get_value( $input ) { if ( ! empty( $_POST[ $input ] ) ) { - return wc_clean( $_POST[ $input ] ); } else { $value = apply_filters( 'woocommerce_checkout_get_value', null, $input ); - if ( $value !== null ) + if ( null !== $value ) { return $value; - - if ( is_user_logged_in() ) { - - $current_user = wp_get_current_user(); - - if ( $meta = get_user_meta( $current_user->ID, $input, true ) ) - return $meta; - - if ( $input == "billing_email" ) - return $current_user->user_email; } - switch ( $input ) { - case "billing_country" : - return apply_filters( 'default_checkout_country', WC()->customer->get_country() ? WC()->customer->get_country() : WC()->countries->get_base_country(), 'billing' ); - case "billing_state" : - return apply_filters( 'default_checkout_state', WC()->customer->has_calculated_shipping() ? WC()->customer->get_state() : '', 'billing' ); - case "billing_postcode" : - return apply_filters( 'default_checkout_postcode', WC()->customer->get_postcode() ? WC()->customer->get_postcode() : '', 'billing' ); - case "shipping_country" : - return apply_filters( 'default_checkout_country', WC()->customer->get_shipping_country() ? WC()->customer->get_shipping_country() : WC()->countries->get_base_country(), 'shipping' ); - case "shipping_state" : - return apply_filters( 'default_checkout_state', WC()->customer->has_calculated_shipping() ? WC()->customer->get_shipping_state() : '', 'shipping' ); - case "shipping_postcode" : - return apply_filters( 'default_checkout_postcode', WC()->customer->get_shipping_postcode() ? WC()->customer->get_shipping_postcode() : '', 'shipping' ); - default : - return apply_filters( 'default_checkout_' . $input, null, $input ); + if ( is_callable( array( WC()->customer, "get_$input" ) ) ) { + $value = WC()->customer->{"get_$input"}() ? WC()->customer->{"get_$input"}() : null; + } elseif ( WC()->customer->meta_exists( $input ) ) { + $value = WC()->customer->get_meta( $input, true ); } + + return apply_filters( 'default_checkout_' . $input, $value, $input ); } } } diff --git a/includes/class-wc-cli.php b/includes/class-wc-cli.php new file mode 100644 index 00000000000..09e55707032 --- /dev/null +++ b/includes/class-wc-cli.php @@ -0,0 +1,42 @@ +includes(); + $this->hooks(); + } + + /** + * Load command files. + */ + private function includes() { + require_once __DIR__ . '/cli/class-wc-cli-runner.php'; + require_once __DIR__ . '/cli/class-wc-cli-rest-command.php'; + require_once __DIR__ . '/cli/class-wc-cli-tool-command.php'; + require_once __DIR__ . '/cli/class-wc-cli-update-command.php'; + } + + /** + * Sets up and hooks WP CLI to our CLI code. + */ + private function hooks() { + WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Runner::after_wp_load' ); + WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tool_Command::register_commands' ); + WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Update_Command::register_commands' ); + } +} + +new WC_CLI; diff --git a/includes/class-wc-comments.php b/includes/class-wc-comments.php index f7573a1796c..c28f73165ff 100644 --- a/includes/class-wc-comments.php +++ b/includes/class-wc-comments.php @@ -7,106 +7,136 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Comments * - * Handle comments (reviews and order notes) + * Handle comments (reviews and order notes). * - * @class WC_Post_types - * @version 2.2.0 - * @package WooCommerce/Classes/Products - * @category Class - * @author WooThemes + * @class WC_Comments + * @version 2.3.0 + * @package WooCommerce/Classes/Products + * @category Class + * @author WooThemes */ class WC_Comments { /** - * Hook in methods + * Hook in methods. */ public static function init() { // Rating posts + add_filter( 'comments_open', array( __CLASS__, 'comments_open' ), 10, 2 ); add_filter( 'preprocess_comment', array( __CLASS__, 'check_comment_rating' ), 0 ); add_action( 'comment_post', array( __CLASS__, 'add_comment_rating' ), 1 ); + add_action( 'comment_moderation_recipients', array( __CLASS__, 'comment_moderation_recipients' ), 10, 2 ); - // clear transients + // Clear transients add_action( 'wp_update_comment_count', array( __CLASS__, 'clear_transients' ) ); // Secure order notes add_filter( 'comments_clauses', array( __CLASS__, 'exclude_order_comments' ), 10, 1 ); - add_action( 'comment_feed_join', array( __CLASS__, 'exclude_order_comments_from_feed_join' ) ); add_action( 'comment_feed_where', array( __CLASS__, 'exclude_order_comments_from_feed_where' ) ); + + // Secure webhook comments + add_filter( 'comments_clauses', array( __CLASS__, 'exclude_webhook_comments' ), 10, 1 ); + add_action( 'comment_feed_where', array( __CLASS__, 'exclude_webhook_comments_from_feed_where' ) ); + + // Count comments + add_filter( 'wp_count_comments', array( __CLASS__, 'wp_count_comments' ), 10, 2 ); + + // Delete comments count cache whenever there is a new comment or a comment status changes + add_action( 'wp_insert_comment', array( __CLASS__, 'delete_comments_count_cache' ) ); + add_action( 'wp_set_comment_status', array( __CLASS__, 'delete_comments_count_cache' ) ); + + // Support avatars for `review` comment type + add_filter( 'get_avatar_comment_types', array( __CLASS__, 'add_avatar_for_review_comment_type' ) ); + + // Review of verified purchase + add_action( 'comment_post', array( __CLASS__, 'add_comment_purchase_verification' ) ); } /** - * Exclude order comments from queries and RSS + * See if comments are open. * - * This code should exclude shop_order comments from queries. Some queries (like the recent comments widget on the dashboard) are hardcoded - * and are not filtered, however, the code current_user_can( 'read_post', $comment->comment_post_ID ) should keep them safe since only admin and + * @since 3.1.0 + * @param bool $open + * @param int $post_id + * @return bool + */ + public static function comments_open( $open, $post_id ) { + if ( 'product' === get_post_type( $post_id ) && ! post_type_supports( 'product', 'comments' ) ) { + $open = false; + } + return $open; + } + + /** + * Exclude order comments from queries and RSS. + * + * This code should exclude shop_order comments from queries. Some queries (like the recent comments widget on the dashboard) are hardcoded. + * and are not filtered, however, the code current_user_can( 'read_post', $comment->comment_post_ID ) should keep them safe since only admin and. * shop managers can view orders anyway. * * The frontend view order pages get around this filter by using remove_filter('comments_clauses', array( 'WC_Comments' ,'exclude_order_comments'), 10, 1 ); - * - * @param array $clauses + * @param array $clauses * @return array */ public static function exclude_order_comments( $clauses ) { - global $wpdb, $typenow; - - if ( is_admin() && $typenow == 'shop_order' && current_user_can( 'manage_woocommerce' ) ) - return $clauses; // Don't hide when viewing orders in admin - - if ( ! $clauses['join'] ) - $clauses['join'] = ''; - - if ( ! strstr( $clauses['join'], "JOIN $wpdb->posts" ) ) - $clauses['join'] .= " LEFT JOIN $wpdb->posts ON comment_post_ID = $wpdb->posts.ID "; - - if ( $clauses['where'] ) - $clauses['where'] .= ' AND '; - - $clauses['where'] .= " $wpdb->posts.post_type NOT IN ('shop_order') "; - + $clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " comment_type != 'order_note' "; return $clauses; } /** - * Exclude order comments from queries and RSS - * - * @param string $join - * @return string + * @deprecated 3.1 */ public static function exclude_order_comments_from_feed_join( $join ) { - global $wpdb; - - if ( ! strstr( $join, $wpdb->posts ) ) - $join = " LEFT JOIN $wpdb->posts ON $wpdb->comments.comment_post_ID = $wpdb->posts.ID "; - - return $join; + wc_deprecated_function( 'WC_Comments::exclude_order_comments_from_feed_join', '3.1' ); } /** - * Exclude order comments from queries and RSS + * Exclude order comments from queries and RSS. * - * @param string $where + * @param string $where * @return string */ public static function exclude_order_comments_from_feed_where( $where ) { - global $wpdb; + return $where . ( $where ? ' AND ' : '' ) . " comment_type != 'order_note' "; + } - if ( $where ) - $where .= ' AND '; + /** + * Exclude webhook comments from queries and RSS. + * @since 2.2 + * @param array $clauses + * @return array + */ + public static function exclude_webhook_comments( $clauses ) { + $clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " comment_type != 'webhook_delivery' "; + return $clauses; + } - $where .= " $wpdb->posts.post_type NOT IN ('shop_order') "; + /** + * @deprecated 3.1 + */ + public static function exclude_webhook_comments_from_feed_join( $join ) { + wc_deprecated_function( 'WC_Comments::exclude_webhook_comments_from_feed_join', '3.1' ); + } - return $where; + /** + * Exclude webhook comments from queries and RSS. + * @since 2.1 + * @param string $where + * @return string + */ + public static function exclude_webhook_comments_from_feed_where( $where ) { + return $where . ( $where ? ' AND ' : '' ) . " comment_type != 'webhook_delivery' "; } /** * Validate the comment ratings. * - * @param array $comment_data + * @param array $comment_data * @return array */ public static function check_comment_rating( $comment_data ) { // If posting a comment (not trackback etc) and not logged in - if ( isset( $_POST['rating'] ) && empty( $_POST['rating'] ) && $comment_data['comment_type'] === '' && get_option('woocommerce_review_rating_required') === 'yes' ) { + if ( ! is_admin() && isset( $_POST['comment_post_ID'], $_POST['rating'], $comment_data['comment_type'] ) && 'product' === get_post_type( $_POST['comment_post_ID'] ) && empty( $_POST['rating'] ) && '' === $comment_data['comment_type'] && 'yes' === get_option( 'woocommerce_enable_review_rating' ) && 'yes' === get_option( 'woocommerce_review_rating_required' ) ) { wp_die( __( 'Please rate the product.', 'woocommerce' ) ); exit; } @@ -115,27 +145,238 @@ class WC_Comments { /** * Rating field for comments. - * - * @param mixed $comment_id + * @param int $comment_id */ public static function add_comment_rating( $comment_id ) { - if ( isset( $_POST['rating'] ) ) { + if ( isset( $_POST['rating'] ) && 'product' === get_post_type( $_POST['comment_post_ID'] ) ) { if ( ! $_POST['rating'] || $_POST['rating'] > 5 || $_POST['rating'] < 0 ) { return; } - add_comment_meta( $comment_id, 'rating', (int) esc_attr( $_POST['rating'] ), true ); + + $post_id = isset( $_POST['comment_post_ID'] ) ? (int) $_POST['comment_post_ID'] : 0; + if ( $post_id ) { + self::clear_transients( $post_id ); + } } } /** - * Clear transients for a review. - * - * @param mixed $comment_id + * Modify recipient of review email. + * @param array $emails + * @param int $comment_id + * @return array + */ + public static function comment_moderation_recipients( $emails, $comment_id ) { + $comment = get_comment( $comment_id ); + + if ( $comment && 'product' === get_post_type( $comment->comment_post_ID ) ) { + $emails = array( get_option( 'admin_email' ) ); + } + + return $emails; + } + + /** + * Ensure product average rating and review count is kept up to date. + * @param int $post_id */ public static function clear_transients( $post_id ) { - delete_transient( 'wc_average_rating_' . absint( $post_id ) ); - delete_transient( 'wc_rating_count_' . absint( $post_id ) ); + + if ( 'product' === get_post_type( $post_id ) ) { + $product = wc_get_product( $post_id ); + self::get_rating_counts_for_product( $product ); + self::get_average_rating_for_product( $product ); + self::get_review_count_for_product( $product ); + } + } + + /** + * Delete comments count cache whenever there is + * new comment or the status of a comment changes. Cache + * will be regenerated next time WC_Comments::wp_count_comments() + * is called. + * + * @return void + */ + public static function delete_comments_count_cache() { + delete_transient( 'wc_count_comments' ); + } + + /** + * Remove order notes and webhook delivery logs from wp_count_comments(). + * + * @since 2.2 + * @param object $stats Comment stats. + * @param int $post_id Post ID. + * @return object + */ + public static function wp_count_comments( $stats, $post_id ) { + global $wpdb; + + if ( 0 === $post_id ) { + $stats = get_transient( 'wc_count_comments' ); + + if ( ! $stats ) { + $stats = array(); + + $count = $wpdb->get_results( " + SELECT comment_approved, COUNT(*) AS num_comments + FROM {$wpdb->comments} + WHERE comment_type NOT IN ('order_note', 'webhook_delivery') + GROUP BY comment_approved + ", ARRAY_A ); + + $total = 0; + $approved = array( + '0' => 'moderated', + '1' => 'approved', + 'spam' => 'spam', + 'trash' => 'trash', + 'post-trashed' => 'post-trashed', + ); + + foreach ( (array) $count as $row ) { + // Don't count post-trashed toward totals. + if ( 'post-trashed' !== $row['comment_approved'] && 'trash' !== $row['comment_approved'] ) { + $total += $row['num_comments']; + } + if ( isset( $approved[ $row['comment_approved'] ] ) ) { + $stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments']; + } + } + + $stats['total_comments'] = $total; + $stats['all'] = $total; + foreach ( $approved as $key ) { + if ( empty( $stats[ $key ] ) ) { + $stats[ $key ] = 0; + } + } + + $stats = (object) $stats; + set_transient( 'wc_count_comments', $stats ); + } + } + + return $stats; + } + + /** + * Make sure WP displays avatars for comments with the `review` type. + * @since 2.3 + * @param array $comment_types + * @return array + */ + public static function add_avatar_for_review_comment_type( $comment_types ) { + return array_merge( $comment_types, array( 'review' ) ); + } + + /** + * Determine if a review is from a verified owner at submission. + * @param int $comment_id + * @return bool + */ + public static function add_comment_purchase_verification( $comment_id ) { + $comment = get_comment( $comment_id ); + $verified = false; + if ( 'product' === get_post_type( $comment->comment_post_ID ) ) { + $verified = wc_customer_bought_product( $comment->comment_author_email, $comment->user_id, $comment->comment_post_ID ); + add_comment_meta( $comment_id, 'verified', (int) $verified, true ); + } + return $verified; + } + + /** + * Get product rating for a product. Please note this is not cached. + * + * @since 3.0.0 + * @param WC_Product $product + * @return float + */ + public static function get_average_rating_for_product( &$product ) { + global $wpdb; + + $count = $product->get_rating_count(); + + if ( $count ) { + $ratings = $wpdb->get_var( $wpdb->prepare(" + SELECT SUM(meta_value) FROM $wpdb->commentmeta + LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID + WHERE meta_key = 'rating' + AND comment_post_ID = %d + AND comment_approved = '1' + AND meta_value > 0 + ", $product->get_id() ) ); + $average = number_format( $ratings / $count, 2, '.', '' ); + } else { + $average = 0; + } + + $product->set_average_rating( $average ); + + $data_store = $product->get_data_store(); + $data_store->update_average_rating( $product ); + + return $average; + } + + /** + * Get product review count for a product (not replies). Please note this is not cached. + * + * @since 3.0.0 + * @param WC_Product $product + * @return int + */ + public static function get_review_count_for_product( &$product ) { + global $wpdb; + + $count = $wpdb->get_var( $wpdb->prepare(" + SELECT COUNT(*) FROM $wpdb->comments + WHERE comment_parent = 0 + AND comment_post_ID = %d + AND comment_approved = '1' + ", $product->get_id() ) ); + + $product->set_review_count( $count ); + + $data_store = $product->get_data_store(); + $data_store->update_review_count( $product ); + + return $count; + } + + /** + * Get product rating count for a product. Please note this is not cached. + * + * @since 3.0.0 + * @param WC_Product $product + * @return array of integers + */ + public static function get_rating_counts_for_product( &$product ) { + global $wpdb; + + $counts = array(); + $raw_counts = $wpdb->get_results( $wpdb->prepare( " + SELECT meta_value, COUNT( * ) as meta_value_count FROM $wpdb->commentmeta + LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID + WHERE meta_key = 'rating' + AND comment_post_ID = %d + AND comment_approved = '1' + AND meta_value > 0 + GROUP BY meta_value + ", $product->get_id() ) ); + + foreach ( $raw_counts as $count ) { + $counts[ $count->meta_value ] = absint( $count->meta_value_count ); + } + + $product->set_rating_counts( $counts ); + + $data_store = $product->get_data_store(); + $data_store->update_rating_counts( $product ); + + return $counts; } } diff --git a/includes/class-wc-countries.php b/includes/class-wc-countries.php index 833cd2cbafe..f9567556fff 100644 --- a/includes/class-wc-countries.php +++ b/includes/class-wc-countries.php @@ -1,14 +1,19 @@ countries ) ) { $this->countries = apply_filters( 'woocommerce_countries', include( WC()->plugin_path() . '/i18n/countries.php' ) ); - if ( apply_filters('woocommerce_sort_countries', true ) ) { + if ( apply_filters( 'woocommerce_sort_countries', true ) ) { asort( $this->countries ); } } @@ -47,7 +51,36 @@ class WC_Countries { } /** - * Load the states + * Get all continents. + * @return array + */ + public function get_continents() { + if ( empty( $this->continents ) ) { + $this->continents = apply_filters( 'woocommerce_continents', include( WC()->plugin_path() . '/i18n/continents.php' ) ); + } + return $this->continents; + } + + /** + * Get continent code for a country code. + * @since 2.6.0 + * @param string $cc string + * @return string + */ + public function get_continent_code_for_country( $cc ) { + $cc = trim( strtoupper( $cc ) ); + $continents = $this->get_continents(); + $continents_and_ccs = wp_list_pluck( $continents, 'countries' ); + foreach ( $continents_and_ccs as $continent_code => $countries ) { + if ( false !== array_search( $cc, $countries ) ) { + return $continent_code; + } + } + return ''; + } + + /** + * Load the states. */ public function load_country_states() { global $states; @@ -56,6 +89,7 @@ class WC_Countries { $states = array( 'AF' => array(), 'AT' => array(), + 'AX' => array(), 'BE' => array(), 'BI' => array(), 'CZ' => array(), @@ -64,25 +98,32 @@ class WC_Countries { 'EE' => array(), 'FI' => array(), 'FR' => array(), + 'GP' => array(), + 'GF' => array(), 'IS' => array(), 'IL' => array(), 'KR' => array(), + 'KW' => array(), + 'LB' => array(), + 'MQ' => array(), 'NL' => array(), 'NO' => array(), 'PL' => array(), 'PT' => array(), + 'RE' => array(), 'SG' => array(), 'SK' => array(), 'SI' => array(), 'LK' => array(), 'SE' => array(), 'VN' => array(), + 'YT' => array(), ); - // Load only the state files the shop owner wants/needs + // Load only the state files the shop owner wants/needs. $allowed = array_merge( $this->get_allowed_countries(), $this->get_shipping_countries() ); - if ( $allowed ) { + if ( ! empty( $allowed ) ) { foreach ( $allowed as $code => $country ) { if ( ! isset( $states[ $code ] ) && file_exists( WC()->plugin_path() . '/i18n/states/' . $code . '.php' ) ) { include( WC()->plugin_path() . '/i18n/states/' . $code . '.php' ); @@ -95,15 +136,14 @@ class WC_Countries { /** * Get the states for a country. - * - * @access public - * @param string $cc country code - * @return array of states + * @param string $cc country code + * @return false|array of states */ public function get_states( $cc = null ) { if ( empty( $this->states ) ) { $this->load_country_states(); } + if ( ! is_null( $cc ) ) { return isset( $this->states[ $cc ] ) ? $this->states[ $cc ] : false; } else { @@ -111,139 +151,174 @@ class WC_Countries { } } + /** + * Get the base address (first line) for the store. + * @since 3.1.1 + * @return string + */ + public function get_base_address() { + $base_address = get_option( 'woocommerce_store_address', '' ); + return apply_filters( 'woocommerce_countries_base_address', $base_address ); + } + + /** + * Get the base address (second line) for the store. + * @since 3.1.1 + * @return string + */ + public function get_base_address_2() { + $base_address_2 = get_option( 'woocommerce_store_address_2', '' ); + return apply_filters( 'woocommerce_countries_base_address_2', $base_address_2 ); + } + /** * Get the base country for the store. - * - * @access public * @return string */ public function get_base_country() { - $default = esc_attr( get_option('woocommerce_default_country') ); - $country = ( ( $pos = strrpos( $default, ':' ) ) === false ) ? $default : substr( $default, 0, $pos ); - - return apply_filters( 'woocommerce_countries_base_country', $country ); + $default = wc_get_base_location(); + return apply_filters( 'woocommerce_countries_base_country', $default['country'] ); } /** * Get the base state for the store. - * - * @access public * @return string */ public function get_base_state() { - $default = wc_clean( get_option( 'woocommerce_default_country' ) ); - $state = ( ( $pos = strrpos( $default, ':' ) ) === false ) ? '' : substr( $default, $pos + 1 ); - - return apply_filters( 'woocommerce_countries_base_state', $state ); + $default = wc_get_base_location(); + return apply_filters( 'woocommerce_countries_base_state', $default['state'] ); } /** * Get the base city for the store. - * - * @access public + * @version 3.1.1 * @return string */ public function get_base_city() { - return apply_filters( 'woocommerce_countries_base_city', '' ); + $base_city = get_option( 'woocommerce_store_city', '' ); + return apply_filters( 'woocommerce_countries_base_city', $base_city ); } /** * Get the base postcode for the store. - * - * @access public + * @since 3.1.1 * @return string */ public function get_base_postcode() { - return apply_filters( 'woocommerce_countries_base_postcode', '' ); + $base_postcode = get_option( 'woocommerce_store_postcode', '' ); + return apply_filters( 'woocommerce_countries_base_postcode', $base_postcode ); } /** * Get the allowed countries for the store. - * - * @access public * @return array */ public function get_allowed_countries() { - if ( get_option('woocommerce_allowed_countries') !== 'specific' ) { + if ( 'all' === get_option( 'woocommerce_allowed_countries' ) ) { return $this->countries; } + if ( 'all_except' === get_option( 'woocommerce_allowed_countries' ) ) { + $except_countries = get_option( 'woocommerce_all_except_countries', array() ); + + if ( ! $except_countries ) { + return $this->countries; + } else { + $all_except_countries = $this->countries; + foreach ( $except_countries as $country ) { + unset( $all_except_countries[ $country ] ); + } + return apply_filters( 'woocommerce_countries_allowed_countries', $all_except_countries ); + } + } + $countries = array(); - $raw_countries = get_option( 'woocommerce_specific_allowed_countries' ); + $raw_countries = get_option( 'woocommerce_specific_allowed_countries', array() ); - foreach ( $raw_countries as $country ) - $countries[ $country ] = $this->countries[ $country ]; + if ( $raw_countries ) { + foreach ( $raw_countries as $country ) { + $countries[ $country ] = $this->countries[ $country ]; + } + } return apply_filters( 'woocommerce_countries_allowed_countries', $countries ); } /** * Get the countries you ship to. - * - * @access public * @return array */ public function get_shipping_countries() { - if ( get_option( 'woocommerce_ship_to_countries' ) == '' ) + if ( '' === get_option( 'woocommerce_ship_to_countries' ) ) { return $this->get_allowed_countries(); + } - if ( get_option('woocommerce_ship_to_countries') !== 'specific' ) + if ( 'all' === get_option( 'woocommerce_ship_to_countries' ) ) { return $this->countries; + } $countries = array(); $raw_countries = get_option( 'woocommerce_specific_ship_to_countries' ); - foreach ( $raw_countries as $country ) - $countries[ $country ] = $this->countries[ $country ]; + if ( $raw_countries ) { + foreach ( $raw_countries as $country ) { + $countries[ $country ] = $this->countries[ $country ]; + } + } return apply_filters( 'woocommerce_countries_shipping_countries', $countries ); } /** - * get_allowed_country_states function. - * - * @access public + * Get allowed country states. * @return array */ public function get_allowed_country_states() { - - if ( get_option('woocommerce_allowed_countries') !== 'specific' ) + if ( get_option( 'woocommerce_allowed_countries' ) !== 'specific' ) { return $this->states; + } $states = array(); $raw_countries = get_option( 'woocommerce_specific_allowed_countries' ); - foreach ( $raw_countries as $country ) - if ( isset( $this->states[ $country ] ) ) - $states[ $country ] = $this->states[ $country ]; + if ( $raw_countries ) { + foreach ( $raw_countries as $country ) { + if ( isset( $this->states[ $country ] ) ) { + $states[ $country ] = $this->states[ $country ]; + } + } + } return apply_filters( 'woocommerce_countries_allowed_country_states', $states ); } /** - * get_shipping_country_states function. - * - * @access public + * Get shipping country states. * @return array */ public function get_shipping_country_states() { - - if ( get_option( 'woocommerce_ship_to_countries' ) == '' ) + if ( get_option( 'woocommerce_ship_to_countries' ) == '' ) { return $this->get_allowed_country_states(); + } - if ( get_option( 'woocommerce_ship_to_countries' ) !== 'specific' ) + if ( get_option( 'woocommerce_ship_to_countries' ) !== 'specific' ) { return $this->states; + } $states = array(); $raw_countries = get_option( 'woocommerce_specific_ship_to_countries' ); - foreach ( $raw_countries as $country ) - if ( ! empty( $this->states[ $country ] ) ) - $states[ $country ] = $this->states[ $country ]; + if ( $raw_countries ) { + foreach ( $raw_countries as $country ) { + if ( ! empty( $this->states[ $country ] ) ) { + $states[ $country ] = $this->states[ $country ]; + } + } + } return apply_filters( 'woocommerce_countries_shipping_country_states', $states ); } @@ -251,184 +326,202 @@ class WC_Countries { /** * Gets an array of countries in the EU. * - * @access public - * @return array + * MC (monaco) and IM (isle of man, part of UK) also use VAT. + * + * @param string $type Type of countries to retrieve. Blank for EU member countries. eu_vat for EU VAT countries. + * @return string[] */ - public function get_european_union_countries() { - return array( 'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HU', 'HR', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK' ); + public function get_european_union_countries( $type = '' ) { + $countries = array( 'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HU', 'HR', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK' ); + + if ( 'eu_vat' === $type ) { + $countries[] = 'MC'; + $countries[] = 'IM'; + } + + return $countries; } /** - * Gets the correct string for shipping - ether 'to the' or 'to' + * Gets the correct string for shipping - either 'to the' or 'to' + * + * @param string $country_code * - * @access public * @return string */ - public function shipping_to_prefix() { - $return = ''; - if (in_array(WC()->customer->get_shipping_country(), array( 'GB', 'US', 'AE', 'CZ', 'DO', 'NL', 'PH', 'USAF' ))) $return = __( 'to the', 'woocommerce' ); - else $return = __( 'to', 'woocommerce' ); - return apply_filters('woocommerce_countries_shipping_to_prefix', $return, WC()->customer->get_shipping_country()); + public function shipping_to_prefix( $country_code = '' ) { + $country_code = $country_code ? $country_code : WC()->customer->get_shipping_country(); + $countries = array( 'GB', 'US', 'AE', 'CZ', 'DO', 'NL', 'PH', 'USAF' ); + $return = in_array( $country_code, $countries ) ? __( 'to the', 'woocommerce' ) : __( 'to', 'woocommerce' ); + + return apply_filters( 'woocommerce_countries_shipping_to_prefix', $return, $country_code ); } /** * Prefix certain countries with 'the' * - * @access public + * @param string $country_code + * * @return string */ - public function estimated_for_prefix() { - $return = ''; - if (in_array($this->get_base_country(), array( 'GB', 'US', 'AE', 'CZ', 'DO', 'NL', 'PH', 'USAF' ))) $return = __( 'the', 'woocommerce' ) . ' '; - return apply_filters('woocommerce_countries_estimated_for_prefix', $return, $this->get_base_country()); + public function estimated_for_prefix( $country_code = '' ) { + $country_code = $country_code ? $country_code : $this->get_base_country(); + $countries = array( 'GB', 'US', 'AE', 'CZ', 'DO', 'NL', 'PH', 'USAF' ); + $return = in_array( $country_code, $countries ) ? __( 'the', 'woocommerce' ) . ' ' : ''; + + return apply_filters( 'woocommerce_countries_estimated_for_prefix', $return, $country_code ); } - /** - * Correctly name tax in some countries VAT on the frontend - * - * @access public + * Correctly name tax in some countries VAT on the frontend. * @return string */ public function tax_or_vat() { - $return = ( in_array($this->get_base_country(), $this->get_european_union_countries()) ) ? __( 'VAT', 'woocommerce' ) : __( 'Tax', 'woocommerce' ); + $return = in_array( $this->get_base_country(), array_merge( $this->get_european_union_countries( 'eu_vat' ), array( 'NO' ) ) ) ? __( 'VAT', 'woocommerce' ) : __( 'Tax', 'woocommerce' ); return apply_filters( 'woocommerce_countries_tax_or_vat', $return ); } /** * Include the Inc Tax label. - * - * @access public * @return string */ public function inc_tax_or_vat() { - $return = ( in_array($this->get_base_country(), $this->get_european_union_countries()) ) ? __( '(incl. VAT)', 'woocommerce' ) : __( '(incl. tax)', 'woocommerce' ); + $return = in_array( $this->get_base_country(), array_merge( $this->get_european_union_countries( 'eu_vat' ), array( 'NO' ) ) ) ? __( '(incl. VAT)', 'woocommerce' ) : __( '(incl. tax)', 'woocommerce' ); return apply_filters( 'woocommerce_countries_inc_tax_or_vat', $return ); } /** * Include the Ex Tax label. - * - * @access public * @return string */ public function ex_tax_or_vat() { - $return = ( in_array($this->get_base_country(), $this->get_european_union_countries()) ) ? __( '(ex. VAT)', 'woocommerce' ) : __( '(ex. tax)', 'woocommerce' ); + $return = in_array( $this->get_base_country(), array_merge( $this->get_european_union_countries( 'eu_vat' ), array( 'NO' ) ) ) ? __( '(ex. VAT)', 'woocommerce' ) : __( '(ex. tax)', 'woocommerce' ); return apply_filters( 'woocommerce_countries_ex_tax_or_vat', $return ); } /** * Outputs the list of countries and states for use in dropdown boxes. - * - * @access public * @param string $selected_country (default: '') * @param string $selected_state (default: '') * @param bool $escape (default: false) - * @return void + * @param bool $escape (default: false) */ public function country_dropdown_options( $selected_country = '', $selected_state = '', $escape = false ) { - if ( $this->countries ) foreach ( $this->countries as $key=>$value) : - if ( $states = $this->get_states( $key ) ) : - echo ''; - foreach ($states as $state_key=>$state_value) : - echo ''; + foreach ( $states as $state_key => $state_value ) : + echo ''; - endforeach; - echo ''; - else : - echo ''. ($escape ? esc_js( $value ) : $value) .''; - endif; - endforeach; + echo '>' . $value . ' — ' . ( $escape ? esc_js( $state_value ) : $state_value ) . ''; + endforeach; + echo ''; + else : + echo '' . ( $escape ? esc_js( $value ) : $value ) . ''; + endif; + endforeach; + endif; } /** - * Get country address formats + * Get country address formats. + * + * These define how addresses are formatted for display in various countries. * - * @access public * @return array */ public function get_address_formats() { - - if (!$this->address_formats) : - - // Common formats - $postcode_before_city = "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}"; - - // Define address formats - $this->address_formats = apply_filters('woocommerce_localisation_address_formats', array( + if ( empty( $this->address_formats ) ) { + $this->address_formats = apply_filters( 'woocommerce_localisation_address_formats', array( 'default' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}", 'AU' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", - 'AT' => $postcode_before_city, - 'BE' => $postcode_before_city, + 'AT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'BE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'CA' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", - 'CH' => $postcode_before_city, + 'CH' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'CL' => "{company}\n{name}\n{address_1}\n{address_2}\n{state}\n{postcode} {city}\n{country}", 'CN' => "{country} {postcode}\n{state}, {city}, {address_2}, {address_1}\n{company}\n{name}", - 'CZ' => $postcode_before_city, - 'DE' => $postcode_before_city, - 'EE' => $postcode_before_city, - 'FI' => $postcode_before_city, - 'DK' => $postcode_before_city, + 'CZ' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'DE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'EE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'FI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'DK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'FR' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city_upper}\n{country}", 'HK' => "{company}\n{first_name} {last_name_upper}\n{address_1}\n{address_2}\n{city_upper}\n{state_upper}\n{country}", 'HU' => "{name}\n{company}\n{city}\n{address_1}\n{address_2}\n{postcode}\n{country}", - 'IS' => $postcode_before_city, + 'IN' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} - {postcode}\n{state}, {country}", + 'IS' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'IT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode}\n{city}\n{state_upper}\n{country}", - 'JP' => "{postcode}\n{state}{city}{address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n {country}", - 'TW' => "{postcode}\n{city}{address_2}\n{address_1}\n{company}\n{last_name} {first_name}\n {country}", - 'LI' => $postcode_before_city, - 'NL' => $postcode_before_city, + 'JP' => "{postcode}\n{state}{city}{address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}", + 'TW' => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}", + 'LI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'NL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'NZ' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", - 'NO' => $postcode_before_city, - 'PL' => $postcode_before_city, - 'SK' => $postcode_before_city, - 'SI' => $postcode_before_city, + 'NO' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'PL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'PT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'ES' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}", - 'SE' => $postcode_before_city, + 'SE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'TR' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city} {state}\n{country}", - 'US' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state} {postcode}\n{country}", + 'US' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state_code} {postcode}\n{country}", 'VN' => "{name}\n{company}\n{address_1}\n{city}\n{country}", - )); - endif; - + ) ); + } return $this->address_formats; } /** - * Get country address format - * - * @access public - * @param array $args (default: array()) + * Get country address format. + * @param array $args (default: array()) * @return string address */ public function get_formatted_address( $args = array() ) { + $default_args = array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + ); - $args = array_map( 'trim', $args ); + $args = array_map( 'trim', wp_parse_args( $args, $default_args ) ); extract( $args ); // Get all formats - $formats = $this->get_address_formats(); + $formats = $this->get_address_formats(); // Get format for the address' country - $format = ( $country && isset( $formats[ $country ] ) ) ? $formats[ $country ] : $formats['default']; + $format = ( $country && isset( $formats[ $country ] ) ) ? $formats[ $country ] : $formats['default']; // Handle full country name - $full_country = ( isset( $this->countries[ $country ] ) ) ? $this->countries[ $country ] : $country; + $full_country = ( isset( $this->countries[ $country ] ) ) ? $this->countries[ $country ] : $country; // Country is not needed if the same as base - if ( $country == $this->get_base_country() && ! apply_filters( 'woocommerce_formatted_address_force_country_display', false ) ) + if ( $country == $this->get_base_country() && ! apply_filters( 'woocommerce_formatted_address_force_country_display', false ) ) { $format = str_replace( '{country}', '', $format ); + } // Handle full state name - $full_state = ( $country && $state && isset( $this->states[ $country ][ $state ] ) ) ? $this->states[ $country ][ $state ] : $state; + $full_state = ( $country && $state && isset( $this->states[ $country ][ $state ] ) ) ? $this->states[ $country ][ $state ] : $state; // Substitute address parts into the string $replace = array_map( 'esc_html', apply_filters( 'woocommerce_formatted_address_replacements', array( @@ -450,6 +543,7 @@ class WC_Countries { '{address_2_upper}' => strtoupper( $address_2 ), '{city_upper}' => strtoupper( $city ), '{state_upper}' => strtoupper( $full_state ), + '{state_code}' => strtoupper( $state ), '{postcode_upper}' => strtoupper( $postcode ), '{country_upper}' => strtoupper( $full_country ), ), $args ) ); @@ -471,8 +565,8 @@ class WC_Countries { } /** - * trim white space and commans off a line - * @param string + * Trim white space and commas off a line. + * @param string $line * @return string */ private function trim_formatted_address_line( $line ) { @@ -481,65 +575,78 @@ class WC_Countries { /** * Returns the fields we show by default. This can be filtered later on. - * - * @access public * @return array */ public function get_default_address_fields() { $fields = array( - 'country' => array( - 'type' => 'country', - 'label' => __( 'Country', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-wide', 'address-field', 'update_totals_on_change' ), + 'first_name' => array( + 'label' => __( 'First name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-first' ), + 'autocomplete' => 'given-name', + 'autofocus' => true, + 'priority' => 10, ), - 'first_name' => array( - 'label' => __( 'First Name', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-first' ), + 'last_name' => array( + 'label' => __( 'Last name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-last' ), + 'autocomplete' => 'family-name', + 'priority' => 20, ), - 'last_name' => array( - 'label' => __( 'Last Name', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-last' ), - 'clear' => true + 'company' => array( + 'label' => __( 'Company name', 'woocommerce' ), + 'class' => array( 'form-row-wide' ), + 'autocomplete' => 'organization', + 'priority' => 30, ), - 'company' => array( - 'label' => __( 'Company Name', 'woocommerce' ), - 'class' => array( 'form-row-wide' ), + 'country' => array( + 'type' => 'country', + 'label' => __( 'Country', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field', 'update_totals_on_change' ), + 'autocomplete' => 'country', + 'priority' => 40, ), - 'address_1' => array( - 'label' => __( 'Address', 'woocommerce' ), - 'placeholder' => _x( 'Street address', 'placeholder', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-wide', 'address-field' ) + 'address_1' => array( + 'label' => __( 'Street address', 'woocommerce' ), + /* translators: use local order of street name and house number. */ + 'placeholder' => esc_attr__( 'House number and street name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'autocomplete' => 'address-line1', + 'priority' => 50, ), - 'address_2' => array( - 'placeholder' => _x( 'Apartment, suite, unit etc. (optional)', 'placeholder', 'woocommerce' ), - 'class' => array( 'form-row-wide', 'address-field' ), - 'required' => false + 'address_2' => array( + 'placeholder' => esc_attr__( 'Apartment, suite, unit etc. (optional)', 'woocommerce' ), + 'class' => array( 'form-row-wide', 'address-field' ), + 'required' => false, + 'autocomplete' => 'address-line2', + 'priority' => 60, ), - 'city' => array( - 'label' => __( 'Town / City', 'woocommerce' ), - 'placeholder' => __( 'Town / City', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-wide', 'address-field' ) + 'city' => array( + 'label' => __( 'Town / City', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'autocomplete' => 'address-level2', + 'priority' => 70, ), - 'state' => array( - 'type' => 'state', - 'label' => __( 'State / County', 'woocommerce' ), - 'placeholder' => __( 'State / County', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-first', 'address-field' ), - 'validate' => array( 'state' ) + 'state' => array( + 'type' => 'state', + 'label' => __( 'State / County', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'validate' => array( 'state' ), + 'autocomplete' => 'address-level1', + 'priority' => 80, ), - 'postcode' => array( - 'label' => __( 'Postcode / Zip', 'woocommerce' ), - 'placeholder' => __( 'Postcode / Zip', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-last', 'address-field' ), - 'clear' => true, - 'validate' => array( 'postcode' ) + 'postcode' => array( + 'label' => __( 'Postcode / ZIP', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'validate' => array( 'postcode' ), + 'autocomplete' => 'postal-code', + 'priority' => 90, ), ); @@ -548,37 +655,36 @@ class WC_Countries { /** * Get JS selectors for fields which are shown/hidden depending on the locale. - * - * @access public * @return array */ public function get_country_locale_field_selectors() { - $locale_fields = array ( - 'address_1' => '#billing_address_1_field, #shipping_address_1_field', - 'address_2' => '#billing_address_2_field, #shipping_address_2_field', - 'state' => '#billing_state_field, #shipping_state_field', - 'postcode' => '#billing_postcode_field, #shipping_postcode_field', - 'city' => '#billing_city_field, #shipping_city_field' + $locale_fields = array( + 'address_1' => '#billing_address_1_field, #shipping_address_1_field', + 'address_2' => '#billing_address_2_field, #shipping_address_2_field', + 'state' => '#billing_state_field, #shipping_state_field, #calc_shipping_state_field', + 'postcode' => '#billing_postcode_field, #shipping_postcode_field, #calc_shipping_postcode_field', + 'city' => '#billing_city_field, #shipping_city_field, #calc_shipping_city_field', ); - return apply_filters( 'woocommerce_country_locale_field_selectors', $locale_fields ); } /** - * Get country locale settings + * Get country locale settings. + * + * These locales override the default country selections after a country is chosen. * - * @access public * @return array */ public function get_country_locale() { - if ( ! $this->locale ) { - - // Locale information used by the checkout - $this->locale = apply_filters('woocommerce_get_country_locale', array( + if ( empty( $this->locale ) ) { + $this->locale = apply_filters( 'woocommerce_get_country_locale', array( 'AE' => array( 'postcode' => array( - 'required' => false, - 'hidden' => true + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'required' => false, ), ), 'AF' => array( @@ -587,24 +693,47 @@ class WC_Countries { ), ), 'AT' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), + ), + 'AU' => array( + 'city' => array( + 'label' => __( 'Suburb', 'woocommerce' ), + ), + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'AX' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), ), 'BD' => array( 'postcode' => array( - 'required' => false + 'required' => false, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), ), - 'state' => array( - 'label' => __( 'District', 'woocommerce' ), - ) ), 'BE' => array( - 'postcode_before_city' => true, + 'postcode' => array( + 'priority' => 65, + ), 'state' => array( - 'required' => false, - 'label' => __( 'Province', 'woocommerce' ), + 'required' => false, + 'label' => __( 'Province', 'woocommerce' ), ), ), 'BI' => array( @@ -614,342 +743,434 @@ class WC_Countries { ), 'BO' => array( 'postcode' => array( - 'required' => false, - 'hidden' => true + 'required' => false, + 'hidden' => true, + ), + ), + 'BS' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, ), ), 'CA' => array( - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ) + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), ), 'CH' => array( - 'postcode_before_city' => true, - 'state' => array( - 'label' => __( 'Canton', 'woocommerce' ), - 'required' => false - ) - ), - 'CL' => array( - 'city' => array( - 'required' => false, + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Canton', 'woocommerce' ), + 'required' => false, + ), + ), + 'CL' => array( + 'city' => array( + 'required' => true, + ), + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'Region', 'woocommerce' ), ), - 'state' => array( - 'label' => __( 'Municipality', 'woocommerce' ), - ) ), 'CN' => array( - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ) + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), ), 'CO' => array( 'postcode' => array( - 'required' => false - ) + 'required' => false, + ), ), 'CZ' => array( - 'state' => array( - 'required' => false - ) + 'state' => array( + 'required' => false, + ), ), 'DE' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), ), 'DK' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), ), 'EE' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), ), 'FI' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), ), 'FR' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), + ), + 'GP' => array( + 'state' => array( + 'required' => false, + ), + ), + 'GF' => array( + 'state' => array( + 'required' => false, + ), ), 'HK' => array( - 'postcode' => array( - 'required' => false + 'postcode' => array( + 'required' => false, ), - 'city' => array( - 'label' => __( 'Town / District', 'woocommerce' ), + 'city' => array( + 'label' => __( 'Town / District', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'Region', 'woocommerce' ), ), - 'state' => array( - 'label' => __( 'Region', 'woocommerce' ), - ) ), 'HU' => array( 'state' => array( - 'label' => __( 'County', 'woocommerce' ), - ) + 'label' => __( 'County', 'woocommerce' ), + ), ), 'ID' => array( - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ) - ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'IE' => array( + 'postcode' => array( + 'required' => false, + 'label' => __( 'Eircode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + ), + ), 'IS' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), ), 'IL' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), ), 'IT' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => true, - 'label' => __( 'Province', 'woocommerce' ), - ) + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => true, + 'label' => __( 'Province', 'woocommerce' ), + ), ), 'JP' => array( - 'state' => array( - 'label' => __( 'Prefecture', 'woocommerce' ) - ) - ), - 'KR' => array( - 'state' => array( - 'required' => false - ) - ), - 'NL' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false, - 'label' => __( 'Province', 'woocommerce' ), - ) - ), - 'NZ' => array( - 'state' => array( - 'required' => false - ) - ), - 'NO' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) - ), - 'PL' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) - ), - 'PT' => array( - 'state' => array( - 'required' => false - ) - ), - 'RO' => array( - 'state' => array( - 'required' => false - ) - ), - 'SG' => array( - 'state' => array( - 'required' => false - ) - ), - 'SK' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) - ), - 'SI' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) - ), - 'ES' => array( - 'postcode_before_city' => true, - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ) - ), - 'LI' => array( - 'postcode_before_city' => true, - 'state' => array( - 'label' => __( 'Municipality', 'woocommerce' ), - 'required' => false - ) - ), - 'LK' => array( - 'state' => array( - 'required' => false - ) - ), - 'SE' => array( - 'postcode_before_city' => true, - 'state' => array( - 'required' => false - ) - ), - 'TR' => array( - 'postcode_before_city' => true, - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ) - ), - 'US' => array( - 'postcode' => array( - 'label' => __( 'Zip', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'State', 'woocommerce' ), - ) - ), - 'GB' => array( - 'postcode' => array( - 'label' => __( 'Postcode', 'woocommerce' ), - ), - 'state' => array( - 'label' => __( 'County', 'woocommerce' ), - 'required' => false - ) - ), - 'VN' => array( - 'state' => array( - 'required' => false + 'state' => array( + 'label' => __( 'Prefecture', 'woocommerce' ), + 'priority' => 66, ), 'postcode' => array( - 'required' => false, - 'hidden' => true + 'priority' => 65, + ), + ), + 'KR' => array( + 'state' => array( + 'required' => false, + ), + ), + 'KW' => array( + 'state' => array( + 'required' => false, + ), + ), + 'LB' => array( + 'state' => array( + 'required' => false, + ), + ), + 'MQ' => array( + 'state' => array( + 'required' => false, + ), + ), + 'NL' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'NZ' => array( + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'required' => false, + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'NO' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), + ), + 'NP' => array( + 'state' => array( + 'label' => __( 'State / Zone', 'woocommerce' ), + ), + 'postcode' => array( + 'required' => false, + ), + ), + 'PL' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), + ), + 'PT' => array( + 'state' => array( + 'required' => false, + ), + ), + 'RE' => array( + 'state' => array( + 'required' => false, + ), + ), + 'RO' => array( + 'state' => array( + 'required' => false, + ), + ), + 'SG' => array( + 'state' => array( + 'required' => false, + ), + ), + 'SK' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), + ), + 'SI' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), + ), + 'ES' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'LI' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Municipality', 'woocommerce' ), + 'required' => false, + ), + ), + 'LK' => array( + 'state' => array( + 'required' => false, + ), + ), + 'SE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + ), + ), + 'TR' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'US' => array( + 'postcode' => array( + 'label' => __( 'ZIP', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'GB' => array( + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + 'required' => false, + ), + ), + 'VN' => array( + 'state' => array( + 'required' => false, + ), + 'postcode' => array( + 'priority' => 65, + 'required' => false, + 'hidden' => false, ), 'address_2' => array( - 'required' => false, - 'hidden' => true - ) + 'required' => false, + 'hidden' => true, + ), ), 'WS' => array( 'postcode' => array( - 'required' => false, - 'hidden' => true + 'required' => false, + 'hidden' => true, + ), + ), + 'YT' => array( + 'state' => array( + 'required' => false, ), ), 'ZA' => array( - 'state' => array( - 'label' => __( 'Province', 'woocommerce' ), - ) + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), ), 'ZW' => array( 'postcode' => array( - 'required' => false, - 'hidden' => true + 'required' => false, + 'hidden' => true, ), ), )); $this->locale = array_intersect_key( $this->locale, array_merge( $this->get_allowed_countries(), $this->get_shipping_countries() ) ); - // Default Locale Can be filters to override fields in get_address_fields(). - // Countries with no specific locale will use default. - $this->locale['default'] = apply_filters('woocommerce_get_country_locale_default', $this->get_default_address_fields() ); + // Default Locale Can be filtered to override fields in get_address_fields(). Countries with no specific locale will use default. + $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_default', $this->get_default_address_fields() ); // Filter default AND shop base locales to allow overides via a single function. These will be used when changing countries on the checkout - if ( ! isset( $this->locale[ $this->get_base_country() ] ) ) + if ( ! isset( $this->locale[ $this->get_base_country() ] ) ) { $this->locale[ $this->get_base_country() ] = $this->locale['default']; + } - $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale['default'] ); - $this->locale[ $this->get_base_country() ] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale[ $this->get_base_country() ] ); + $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale['default'] ); + $this->locale[ $this->get_base_country() ] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale[ $this->get_base_country() ] ); } return $this->locale; } /** - * Apply locale and get address fields - * - * @access public - * @param mixed $country - * @param string $type (default: 'billing_') + * Apply locale and get address fields. + * @param mixed $country (default: '') + * @param string $type (default: 'billing_') * @return array */ - public function get_address_fields( $country, $type = 'billing_' ) { + public function get_address_fields( $country = '', $type = 'billing_' ) { + if ( ! $country ) { + $country = $this->get_base_country(); + } - if (!$country) - $country = $this->get_base_country(); - - $fields = $this->get_default_address_fields(); - $locale = $this->get_country_locale(); + $fields = $this->get_default_address_fields(); + $locale = $this->get_country_locale(); if ( isset( $locale[ $country ] ) ) { - $fields = wc_array_overlay( $fields, $locale[ $country ] ); - - // If default country has postcode_before_city switch the fields round. - // This is only done at this point, not if country changes on checkout. - if ( isset( $locale[ $country ]['postcode_before_city'] ) ) { - if ( isset( $fields['postcode'] ) ) { - $fields['postcode']['class'] = array( 'form-row-wide', 'address-field' ); - - $switch_fields = array(); - - foreach ( $fields as $key => $value ) { - if ( $key == 'city' ) { - // Place postcode before city - $switch_fields['postcode'] = ''; - } - $switch_fields[$key] = $value; - } - - $fields = $switch_fields; - } - } } // Prepend field keys $address_fields = array(); foreach ( $fields as $key => $value ) { - $address_fields[$type . $key] = $value; + if ( 'state' === $key ) { + $value['country_field'] = $type . 'country'; + } + $address_fields[ $type . $key ] = $value; } - // Billing/Shipping Specific - if ( $type == 'billing_' ) { - - $address_fields['billing_email'] = array( - 'label' => __( 'Email Address', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-first' ), - 'validate' => array( 'email' ), - ); + // Add email and phone fields. + if ( 'billing_' === $type ) { $address_fields['billing_phone'] = array( - 'label' => __( 'Phone', 'woocommerce' ), - 'required' => true, - 'class' => array( 'form-row-last' ), - 'clear' => true, - 'validate' => array( 'phone' ), + 'label' => __( 'Phone', 'woocommerce' ), + 'required' => true, + 'type' => 'tel', + 'class' => array( 'form-row-first' ), + 'validate' => array( 'phone' ), + 'autocomplete' => 'tel', + 'priority' => 100, + ); + $address_fields['billing_email'] = array( + 'label' => __( 'Email address', 'woocommerce' ), + 'required' => true, + 'type' => 'email', + 'class' => array( 'form-row-last' ), + 'validate' => array( 'email' ), + 'autocomplete' => 'no' === get_option( 'woocommerce_registration_generate_username' ) ? 'email' : 'email username', + 'priority' => 110, ); - } - $address_fields = apply_filters( 'woocommerce_' . $type . 'fields', $address_fields, $country ); - - // Return - return $address_fields; + /** + * Important note on this filter: Changes to address fields can and will be overridden by + * the woocommerce_default_address_fields. The locales/default locales apply on top based + * on country selection. If you want to change things like the required status of an + * address field, filter woocommerce_default_address_fields instead. + */ + return apply_filters( 'woocommerce_' . $type . 'fields', $address_fields, $country ); } } diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php index 5306810832a..d61b0eb5016 100644 --- a/includes/class-wc-coupon.php +++ b/includes/class-wc-coupon.php @@ -1,15 +1,52 @@ '', + 'amount' => 0, + 'date_created' => null, + 'date_modified' => null, + 'date_expires' => null, + 'discount_type' => 'fixed_cart', + 'description' => '', + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => 0, + 'usage_limit_per_user' => 0, + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'email_restrictions' => array(), + 'used_by' => array(), + ); // Coupon message codes const E_WC_COUPON_INVALID_FILTERED = 100; @@ -24,239 +61,697 @@ class WC_Coupon { const E_WC_COUPON_NOT_APPLICABLE = 109; const E_WC_COUPON_NOT_VALID_SALE_ITEMS = 110; const E_WC_COUPON_PLEASE_ENTER = 111; + const E_WC_COUPON_MAX_SPEND_LIMIT_MET = 112; + const E_WC_COUPON_EXCLUDED_PRODUCTS = 113; + const E_WC_COUPON_EXCLUDED_CATEGORIES = 114; const WC_COUPON_SUCCESS = 200; const WC_COUPON_REMOVED = 201; - /** @public string Coupon code. */ - public $code; - - /** @public int Coupon ID. */ - public $id; - - /** @public string Type of discount. */ - public $type; - - /** @public string Type of discount (alias). */ - public $discount_type; - - /** @public string Coupon amount. */ - public $amount; - - /** @public string "Yes" if for individual use. */ - public $individual_use; - - /** @public array Array of product IDs. */ - public $product_ids; - - /** @public int Coupon usage limit. */ - public $usage_limit; - - /** @public int Coupon usage limit per user. */ - public $usage_limit_per_user; - - /** @public int Coupon usage limit per item. */ - public $limit_usage_to_x_items; - - /** @public int Coupon usage count. */ - public $usage_count; - - /** @public string Expiry date. */ - public $expiry_date; - - /** @public string "yes" if applied before tax. */ - public $apply_before_tax; - - /** @public string "yes" if coupon grants free shipping. */ - public $free_shipping; - - /** @public array Array of category ids. */ - public $product_categories; - - /** @public array Array of category ids. */ - public $exclude_product_categories; - - /** @public string "yes" if coupon does NOT apply to items on sale. */ - public $exclude_sale_items; - - /** @public string Minimum cart amount. */ - public $minimum_amount; - - /** @public string Coupon owner's email. */ - public $customer_email; - - /** @public array Post meta. */ - public $coupon_custom_fields; - - /** @public string How much the coupon is worth. */ - public $coupon_amount; - - /** @public string Error message. */ - public $error_message; + /** + * Cache group. + * @var string + */ + protected $cache_group = 'coupons'; /** * Coupon constructor. Loads coupon data. - * - * @access public - * @param mixed $code code of the coupon to load + * @param mixed $data Coupon data, object, ID or code. */ - public function __construct( $code ) { - global $wpdb; + public function __construct( $data = '' ) { + parent::__construct( $data ); - $this->code = apply_filters( 'woocommerce_coupon_code', $code ); + if ( $data instanceof WC_Coupon ) { + $this->set_id( absint( $data->get_id() ) ); + } elseif ( $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data ) ) { + $this->read_manual_coupon( $data, $coupon ); + return; + } elseif ( is_int( $data ) && 'shop_coupon' === get_post_type( $data ) ) { + $this->set_id( $data ); + } elseif ( ! empty( $data ) ) { + $id = wc_get_coupon_id_by_code( $data ); - // Coupon data lets developers create coupons through code - $coupon_data = apply_filters( 'woocommerce_get_shop_coupon_data', false, $code ); - - if ( $coupon_data ) { - - $this->id = absint( $coupon_data['id'] ); - $this->type = esc_html( $coupon_data['type'] ); - $this->amount = esc_html( $coupon_data['amount'] ); - $this->individual_use = esc_html( $coupon_data['individual_use'] ); - $this->product_ids = is_array( $coupon_data['product_ids'] ) ? $coupon_data['product_ids'] : array(); - $this->exclude_product_ids = is_array( $coupon_data['exclude_product_ids'] ) ? $coupon_data['exclude_product_ids'] : array(); - $this->usage_limit = absint( $coupon_data['usage_limit'] ); - $this->usage_limit_per_user = isset( $coupon_data['usage_limit_per_user'] ) ? absint( $coupon_data['usage_limit_per_user'] ) : 0; - $this->limit_usage_to_x_items = isset( $coupon_data['limit_usage_to_x_items'] ) ? absint( $coupon_data['limit_usage_to_x_items'] ) : ''; - $this->usage_count = absint( $coupon_data['usage_count'] ); - $this->expiry_date = esc_html( $coupon_data['expiry_date'] ); - $this->apply_before_tax = esc_html( $coupon_data['apply_before_tax'] ); - $this->free_shipping = esc_html( $coupon_data['free_shipping'] ); - $this->product_categories = is_array( $coupon_data['product_categories'] ) ? $coupon_data['product_categories'] : array(); - $this->exclude_product_categories = is_array( $coupon_data['exclude_product_categories'] ) ? $coupon_data['exclude_product_categories'] : array(); - $this->exclude_sale_items = esc_html( $coupon_data['exclude_sale_items'] ); - $this->minimum_amount = esc_html( $coupon_data['minimum_amount'] ); - $this->customer_email = esc_html( $coupon_data['customer_email'] ); - - } else { - - $coupon_id = $wpdb->get_var( $wpdb->prepare( apply_filters( 'woocommerce_coupon_code_query', "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'" ), $this->code ) ); - - if ( ! $coupon_id ) - return; - - $coupon = get_post( $coupon_id ); - $this->post_title = apply_filters( 'woocommerce_coupon_code', $coupon->post_title ); - - if ( empty( $coupon ) || $this->code !== $this->post_title ) - return; - - $this->id = $coupon->ID; - $this->coupon_custom_fields = get_post_meta( $this->id ); - - $load_data = array( - 'discount_type' => 'fixed_cart', - 'coupon_amount' => 0, - 'individual_use' => 'no', - 'product_ids' => '', - 'exclude_product_ids' => '', - 'usage_limit' => '', - 'usage_limit_per_user' => '', - 'limit_usage_to_x_items' => '', - 'usage_count' => '', - 'expiry_date' => '', - 'apply_before_tax' => 'yes', - 'free_shipping' => 'no', - 'product_categories' => array(), - 'exclude_product_categories' => array(), - 'exclude_sale_items' => 'no', - 'minimum_amount' => '', - 'customer_email' => array() - ); - - foreach ( $load_data as $key => $default ) - $this->$key = isset( $this->coupon_custom_fields[ $key ][0] ) && $this->coupon_custom_fields[ $key ][0] !== '' ? $this->coupon_custom_fields[ $key ][0] : $default; - - // Alias - $this->type = $this->discount_type; - $this->amount = $this->coupon_amount; - - // Formatting - $this->product_ids = array_filter( array_map( 'trim', explode( ',', $this->product_ids ) ) ); - $this->exclude_product_ids = array_filter( array_map( 'trim', explode( ',', $this->exclude_product_ids ) ) ); - $this->expiry_date = $this->expiry_date ? strtotime( $this->expiry_date ) : ''; - $this->product_categories = array_filter( array_map( 'trim', (array) maybe_unserialize( $this->product_categories ) ) ); - $this->exclude_product_categories = array_filter( array_map( 'trim', (array) maybe_unserialize( $this->exclude_product_categories ) ) ); - $this->customer_email = array_filter( array_map( 'trim', array_map( 'strtolower', (array) maybe_unserialize( $this->customer_email ) ) ) ); - } - - do_action( 'woocommerce_coupon_loaded', $this ); - } - - - /** - * Check if coupon needs applying before tax. - * - * @access public - * @return bool - */ - public function apply_before_tax() { - return $this->apply_before_tax == 'yes' ? true : false; - } - - - /** - * Check if a coupon enables free shipping. - * - * @access public - * @return bool - */ - public function enable_free_shipping() { - return $this->free_shipping == 'yes' ? true : false; - } - - - /** - * Check if a coupon excludes sale items. - * - * @access public - * @return bool - */ - public function exclude_sale_items() { - return $this->exclude_sale_items == 'yes' ? true : false; - } - - - - /** - * Increase usage count fo current coupon. - * - * @access public - * @param string $used_by Either user ID or billing email - * @return void - */ - public function inc_usage_count( $used_by = '' ) { - $this->usage_count++; - update_post_meta( $this->id, 'usage_count', $this->usage_count ); - - if ( $used_by ) { - add_post_meta( $this->id, '_used_by', strtolower( $used_by ) ); + // Need to support numeric strings for backwards compatibility. + if ( ! $id && 'shop_coupon' === get_post_type( $data ) ) { + $this->set_id( $data ); + } else { + $this->set_id( $id ); + $this->set_code( $data ); + } + } else { + $this->set_object_read( true ); } - } - - /** - * Decrease usage count fo current coupon. - * - * @access public - * @param string $used_by Either user ID or billing email - * @return void - */ - public function dcr_usage_count( $used_by = '' ) { - global $wpdb; - - $this->usage_count--; - update_post_meta( $this->id, 'usage_count', $this->usage_count ); - - // Delete 1 used by meta - $meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", $used_by, $this->id ) ); - if ( $meta_id ) { - delete_metadata_by_mid( 'post', $meta_id ); + $this->data_store = WC_Data_Store::load( 'coupon' ); + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); } } /** - * Returns the error_message string + * Checks the coupon type. + * @param string $type Array or string of types + * @return bool + */ + public function is_type( $type ) { + return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type ) ) ); + } + + /** + * Prefix for action and filter hooks on data. + * + * @since 3.0.0 + * @return string + */ + protected function get_hook_prefix() { + return 'woocommerce_coupon_get_'; + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the coupon object. + | + */ + + /** + * Get coupon code. + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_code( $context = 'view' ) { + return $this->get_prop( 'code', $context ); + } + + /** + * Get coupon description. + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_description( $context = 'view' ) { + return $this->get_prop( 'description', $context ); + } + + /** + * Get discount type. + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_discount_type( $context = 'view' ) { + return $this->get_prop( 'discount_type', $context ); + } + + /** + * Get coupon amount. + * @since 3.0.0 + * @param string $context + * @return float + */ + public function get_amount( $context = 'view' ) { + return $this->get_prop( 'amount', $context ); + } + + /** + * Get coupon expiration date. + * @since 3.0.0 + * @param string $context + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_expires( $context = 'view' ) { + return $this->get_prop( 'date_expires', $context ); + } + + /** + * Get date_created + * @since 3.0.0 + * @param string $context + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Get date_modified + * @since 3.0.0 + * @param string $context + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Get coupon usage count. + * @since 3.0.0 + * @param string $context + * @return integer + */ + public function get_usage_count( $context = 'view' ) { + return $this->get_prop( 'usage_count', $context ); + } + + /** + * Get the "indvidual use" checkbox status. + * @since 3.0.0 + * @param string $context + * @return bool + */ + public function get_individual_use( $context = 'view' ) { + return $this->get_prop( 'individual_use', $context ); + } + + /** + * Get product IDs this coupon can apply to. + * @since 3.0.0 + * @param string $context + * @return array + */ + public function get_product_ids( $context = 'view' ) { + return $this->get_prop( 'product_ids', $context ); + } + + /** + * Get product IDs that this coupon should not apply to. + * @since 3.0.0 + * @param string $context + * @return array + */ + public function get_excluded_product_ids( $context = 'view' ) { + return $this->get_prop( 'excluded_product_ids', $context ); + } + + /** + * Get coupon usage limit. + * @since 3.0.0 + * @param string $context + * @return integer + */ + public function get_usage_limit( $context = 'view' ) { + return $this->get_prop( 'usage_limit', $context ); + } + + /** + * Get coupon usage limit per customer (for a single customer) + * @since 3.0.0 + * @param string $context + * @return integer + */ + public function get_usage_limit_per_user( $context = 'view' ) { + return $this->get_prop( 'usage_limit_per_user', $context ); + } + + /** + * Usage limited to certain amount of items + * @since 3.0.0 + * @param string $context + * @return integer|null + */ + public function get_limit_usage_to_x_items( $context = 'view' ) { + return $this->get_prop( 'limit_usage_to_x_items', $context ); + } + + /** + * If this coupon grants free shipping or not. + * @since 3.0.0 + * @param string $context + * @return bool + */ + public function get_free_shipping( $context = 'view' ) { + return $this->get_prop( 'free_shipping', $context ); + } + + /** + * Get product categories this coupon can apply to. + * @since 3.0.0 + * @param string $context + * @return array + */ + public function get_product_categories( $context = 'view' ) { + return $this->get_prop( 'product_categories', $context ); + } + + /** + * Get product categories this coupon cannot not apply to. + * @since 3.0.0 + * @param string $context + * @return array + */ + public function get_excluded_product_categories( $context = 'view' ) { + return $this->get_prop( 'excluded_product_categories', $context ); + } + + /** + * If this coupon should exclude items on sale. + * @since 3.0.0 + * @param string $context + * @return bool + */ + public function get_exclude_sale_items( $context = 'view' ) { + return $this->get_prop( 'exclude_sale_items', $context ); + } + + /** + * Get minium spend amount. + * @since 3.0.0 + * @param string $context + * @return float + */ + public function get_minimum_amount( $context = 'view' ) { + return $this->get_prop( 'minimum_amount', $context ); + } + /** + * Get maximum spend amount. + * @since 3.0.0 + * @param string $context + * @return float + */ + public function get_maximum_amount( $context = 'view' ) { + return $this->get_prop( 'maximum_amount', $context ); + } + + /** + * Get emails to check customer usage restrictions. + * @since 3.0.0 + * @param string $context + * @return array + */ + public function get_email_restrictions( $context = 'view' ) { + return $this->get_prop( 'email_restrictions', $context ); + } + + /** + * Get records of all users who have used the current coupon. + * @since 3.0.0 + * @param string $context + * @return array + */ + public function get_used_by( $context = 'view' ) { + return $this->get_prop( 'used_by', $context ); + } + + /** + * Get discount amount for a cart item. + * + * @param float $discounting_amount Amount the coupon is being applied to + * @param array|null $cart_item Cart item being discounted if applicable + * @param boolean $single True if discounting a single qty item, false if its the line + * @return float Amount this coupon has discounted + */ + public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) { + $discount = 0; + $cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity']; + + if ( $this->is_type( array( 'percent' ) ) ) { + $discount = (float) $this->get_amount() * ( $discounting_amount / 100 ); + } elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) { + /** + * This is the most complex discount - we need to divide the discount between rows based on their price in. + * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows. + * with no price (free) don't get discounted. + * + * Get item discount by dividing item cost by subtotal to get a %. + * + * Uses price inc tax if prices include tax to work around https://github.com/woocommerce/woocommerce/issues/7669 and https://github.com/woocommerce/woocommerce/issues/8074. + */ + if ( wc_prices_include_tax() ) { + $discount_percent = ( wc_get_price_including_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal; + } else { + $discount_percent = ( wc_get_price_excluding_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal_ex_tax; + } + $discount = ( (float) $this->get_amount() * $discount_percent ) / $cart_item_qty; + + } elseif ( $this->is_type( 'fixed_product' ) ) { + $discount = min( $this->get_amount(), $discounting_amount ); + $discount = $single ? $discount : $discount * $cart_item_qty; + } + + $discount = (float) min( $discount, $discounting_amount ); + + // Handle the limit_usage_to_x_items option + if ( ! $this->is_type( array( 'fixed_cart' ) ) ) { + if ( $discounting_amount ) { + if ( null === $this->get_limit_usage_to_x_items() ) { + $limit_usage_qty = $cart_item_qty; + } else { + $limit_usage_qty = min( $this->get_limit_usage_to_x_items(), $cart_item_qty ); + + $this->set_limit_usage_to_x_items( max( 0, ( $this->get_limit_usage_to_x_items() - $limit_usage_qty ) ) ); + } + if ( $single ) { + $discount = ( $discount * $limit_usage_qty ) / $cart_item_qty; + } else { + $discount = ( $discount / $cart_item_qty ) * $limit_usage_qty; + } + } + } + + $discount = round( $discount, wc_get_rounding_precision() ); + + return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Functions for setting coupon data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. + | + */ + + /** + * Set coupon code. + * @since 3.0.0 + * @param string $code + * @throws WC_Data_Exception + */ + public function set_code( $code ) { + $this->set_prop( 'code', wc_format_coupon_code( $code ) ); + } + + /** + * Set coupon description. + * @since 3.0.0 + * @param string $description + * @throws WC_Data_Exception + */ + public function set_description( $description ) { + $this->set_prop( 'description', $description ); + } + + /** + * Set discount type. + * @since 3.0.0 + * @param string $discount_type + * @throws WC_Data_Exception + */ + public function set_discount_type( $discount_type ) { + if ( 'percent_product' === $discount_type ) { + $discount_type = 'percent'; // Backwards compatibility. + } + if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ) ) ) { + $this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) ); + } + $this->set_prop( 'discount_type', $discount_type ); + } + + /** + * Set amount. + * @since 3.0.0 + * @param float $amount + * @throws WC_Data_Exception + */ + public function set_amount( $amount ) { + $this->set_prop( 'amount', wc_format_decimal( $amount ) ); + } + + /** + * Set expiration date. + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + * @throws WC_Data_Exception + */ + public function set_date_expires( $date ) { + $this->set_date_prop( 'date_expires', $date ); + } + + /** + * Set date_created + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + * @throws WC_Data_Exception + */ + public function set_date_created( $date ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set date_modified + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + * @throws WC_Data_Exception + */ + public function set_date_modified( $date ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set how many times this coupon has been used. + * @since 3.0.0 + * @param int $usage_count + * @throws WC_Data_Exception + */ + public function set_usage_count( $usage_count ) { + $this->set_prop( 'usage_count', absint( $usage_count ) ); + } + + /** + * Set if this coupon can only be used once. + * @since 3.0.0 + * @param bool $is_individual_use + * @throws WC_Data_Exception + */ + public function set_individual_use( $is_individual_use ) { + $this->set_prop( 'individual_use', (bool) $is_individual_use ); + } + + /** + * Set the product IDs this coupon can be used with. + * @since 3.0.0 + * @param array $product_ids + * @throws WC_Data_Exception + */ + public function set_product_ids( $product_ids ) { + $this->set_prop( 'product_ids', array_filter( wp_parse_id_list( (array) $product_ids ) ) ); + } + + /** + * Set the product IDs this coupon cannot be used with. + * @since 3.0.0 + * @param array $excluded_product_ids + * @throws WC_Data_Exception + */ + public function set_excluded_product_ids( $excluded_product_ids ) { + $this->set_prop( 'excluded_product_ids', array_filter( wp_parse_id_list( (array) $excluded_product_ids ) ) ); + } + + /** + * Set the amount of times this coupon can be used. + * @since 3.0.0 + * @param int $usage_limit + * @throws WC_Data_Exception + */ + public function set_usage_limit( $usage_limit ) { + $this->set_prop( 'usage_limit', absint( $usage_limit ) ); + } + + /** + * Set the amount of times this coupon can be used per user. + * @since 3.0.0 + * @param int $usage_limit + * @throws WC_Data_Exception + */ + public function set_usage_limit_per_user( $usage_limit ) { + $this->set_prop( 'usage_limit_per_user', absint( $usage_limit ) ); + } + + /** + * Set usage limit to x number of items. + * @since 3.0.0 + * @param int|null $limit_usage_to_x_items + * @throws WC_Data_Exception + */ + public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) { + $this->set_prop( 'limit_usage_to_x_items', is_null( $limit_usage_to_x_items ) ? null : absint( $limit_usage_to_x_items ) ); + } + + /** + * Set if this coupon enables free shipping or not. + * @since 3.0.0 + * @param bool $free_shipping + * @throws WC_Data_Exception + */ + public function set_free_shipping( $free_shipping ) { + $this->set_prop( 'free_shipping', (bool) $free_shipping ); + } + + /** + * Set the product category IDs this coupon can be used with. + * @since 3.0.0 + * @param array $product_categories + * @throws WC_Data_Exception + */ + public function set_product_categories( $product_categories ) { + $this->set_prop( 'product_categories', array_filter( wp_parse_id_list( (array) $product_categories ) ) ); + } + + /** + * Set the product category IDs this coupon cannot be used with. + * @since 3.0.0 + * @param array $excluded_product_categories + * @throws WC_Data_Exception + */ + public function set_excluded_product_categories( $excluded_product_categories ) { + $this->set_prop( 'excluded_product_categories', array_filter( wp_parse_id_list( (array) $excluded_product_categories ) ) ); + } + + /** + * Set if this coupon should excluded sale items or not. + * @since 3.0.0 + * @param bool $exclude_sale_items + * @throws WC_Data_Exception + */ + public function set_exclude_sale_items( $exclude_sale_items ) { + $this->set_prop( 'exclude_sale_items', (bool) $exclude_sale_items ); + } + + /** + * Set the minimum spend amount. + * @since 3.0.0 + * @param float $amount + * @throws WC_Data_Exception + */ + public function set_minimum_amount( $amount ) { + $this->set_prop( 'minimum_amount', wc_format_decimal( $amount ) ); + } + + /** + * Set the maximum spend amount. + * @since 3.0.0 + * @param float $amount + * @throws WC_Data_Exception + */ + public function set_maximum_amount( $amount ) { + $this->set_prop( 'maximum_amount', wc_format_decimal( $amount ) ); + } + + /** + * Set email restrictions. + * @since 3.0.0 + * @param array $emails + * @throws WC_Data_Exception + */ + public function set_email_restrictions( $emails = array() ) { + $emails = array_filter( array_map( 'sanitize_email', array_map( 'strtolower', (array) $emails ) ) ); + foreach ( $emails as $email ) { + if ( ! is_email( $email ) ) { + $this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) ); + } + } + $this->set_prop( 'email_restrictions', $emails ); + } + + /** + * Set which users have used this coupon. + * @since 3.0.0 + * @param array $used_by + * @throws WC_Data_Exception + */ + public function set_used_by( $used_by ) { + $this->set_prop( 'used_by', array_filter( $used_by ) ); + } + + /* + |-------------------------------------------------------------------------- + | Other Actions + |-------------------------------------------------------------------------- + */ + + /** + * Developers can programically return coupons. This function will read those values into our WC_Coupon class. + * @since 3.0.0 + * @param string $code Coupon code + * @param array $coupon Array of coupon properties + */ + public function read_manual_coupon( $code, $coupon ) { + foreach ( $coupon as $key => $value ) { + switch ( $key ) { + case 'excluded_product_ids' : + case 'exclude_product_ids' : + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon['excluded_product_ids'] = wc_string_to_array( $value ); + } + break; + case 'exclude_product_categories' : + case 'excluded_product_categories' : + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon['excluded_product_categories'] = wc_string_to_array( $value ); + } + break; + case 'product_ids' : + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon[ $key ] = wc_string_to_array( $value ); + } + break; + case 'individual_use' : + case 'free_shipping' : + case 'exclude_sale_items' : + if ( ! is_bool( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be true or false instead of yes or no.', '3.0' ); + $coupon[ $key ] = wc_string_to_bool( $value ); + } + break; + case 'expiry_date' : + $coupon['date_expires'] = $value; + break; + } + } + $this->set_code( $code ); + $this->set_props( $coupon ); + } + + /** + * Increase usage count for current coupon. + * + * @param string $used_by Either user ID or billing email + */ + public function increase_usage_count( $used_by = '' ) { + if ( $this->get_id() && $this->data_store ) { + $new_count = $this->data_store->increase_usage_count( $this, $used_by ); + + // Bypass set_prop and remove pending changes since the data store saves the count already. + $this->data['usage_count'] = $new_count; + if ( isset( $this->changes['usage_count'] ) ) { + unset( $this->changes['usage_count'] ); + } + } + } + + /** + * Decrease usage count for current coupon. + * + * @param string $used_by Either user ID or billing email + */ + public function decrease_usage_count( $used_by = '' ) { + if ( $this->get_id() && $this->get_usage_count() > 0 && $this->data_store ) { + $new_count = $this->data_store->decrease_usage_count( $this, $used_by ); + + // Bypass set_prop and remove pending changes since the data store saves the count already. + $this->data['usage_count'] = $new_count; + if ( isset( $this->changes['usage_count'] ) ) { + unset( $this->changes['usage_count'] ); + } + } + } + + /* + |-------------------------------------------------------------------------- + | Validation & Error Handling + |-------------------------------------------------------------------------- + */ + + /** + * Returns the error_message string. * * @access public * @return string @@ -266,329 +761,355 @@ class WC_Coupon { } /** - * is_valid function. + * Ensure coupon exists or throw exception. * - * Check if a coupon is valid. Return a reason code if invalid. Reason codes: - * - * @access public - * @return bool|WP_Error validity or a WP_Error if not valid + * @throws Exception */ - public function is_valid() { - - $error_code = null; - $valid = true; - $error = false; - - if ( $this->id ) { - - // Usage Limit - if ( $this->usage_limit > 0 ) { - if ( $this->usage_count >= $this->usage_limit ) { - $valid = false; - $error_code = self::E_WC_COUPON_USAGE_LIMIT_REACHED; - } - } - - // Per user usage limit - check here if user is logged in (against user IDs) - // Checked again for emails later on in WC_Cart::check_customer_coupons() - if ( $this->usage_limit_per_user > 0 && is_user_logged_in() ) { - $used_by = (array) get_post_meta( $this->id, '_used_by' ); - $usage_count = sizeof( array_keys( $used_by, get_current_user_id() ) ); - - if ( $usage_count >= $this->usage_limit_per_user ) { - $valid = false; - $error_code = self::E_WC_COUPON_USAGE_LIMIT_REACHED; - } - } - - // Expired - if ( $this->expiry_date ) { - if ( current_time( 'timestamp' ) > $this->expiry_date ) { - $valid = false; - $error_code = self::E_WC_COUPON_EXPIRED; - } - } - - // Minimum spend - if ( $this->minimum_amount > 0 ) { - if ( $this->minimum_amount > WC()->cart->subtotal ) { - $valid = false; - $error_code = self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET; - } - } - - // Product ids - If a product included is found in the cart then its valid - if ( sizeof( $this->product_ids ) > 0 ) { - $valid_for_cart = false; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - - if ( in_array( $cart_item['product_id'], $this->product_ids ) || in_array( $cart_item['variation_id'], $this->product_ids ) || in_array( $cart_item['data']->get_parent(), $this->product_ids ) ) - $valid_for_cart = true; - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_APPLICABLE; - } - } - - // Category ids - If a product included is found in the cart then its valid - if ( sizeof( $this->product_categories ) > 0 ) { - $valid_for_cart = false; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - - $product_cats = wp_get_post_terms($cart_item['product_id'], 'product_cat', array("fields" => "ids")); - - if ( sizeof( array_intersect( $product_cats, $this->product_categories ) ) > 0 ) - $valid_for_cart = true; - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_APPLICABLE; - } - } - - // Exclude Sale Items check for product coupons - valid if a non-sale item is present - if ( 'yes' === $this->exclude_sale_items && in_array( $this->type, array( 'fixed_product', 'percent_product' ) ) ) { - $valid_for_cart = false; - $product_ids_on_sale = wc_get_product_ids_on_sale(); - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - if ( sizeof( array_intersect( array( absint( $cart_item['product_id'] ), absint( $cart_item['variation_id'] ), $cart_item['data']->get_parent() ), $product_ids_on_sale ) ) === 0 ) { - // not on sale - $valid_for_cart = true; - } - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_VALID_SALE_ITEMS; - } - } - - // Cart discounts cannot be added if non-eligble product is found in cart - if ( $this->type != 'fixed_product' && $this->type != 'percent_product' ) { - - // Exclude Products - if ( sizeof( $this->exclude_product_ids ) > 0 ) { - $valid_for_cart = true; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - if ( in_array( $cart_item['product_id'], $this->exclude_product_ids ) || in_array( $cart_item['variation_id'], $this->exclude_product_ids ) || in_array( $cart_item['data']->get_parent(), $this->exclude_product_ids ) ) { - $valid_for_cart = false; - } - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_APPLICABLE; - } - } - - // Exclude Sale Items - if ( $this->exclude_sale_items == 'yes' ) { - $valid_for_cart = true; - $product_ids_on_sale = wc_get_product_ids_on_sale(); - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - if ( in_array( $cart_item['product_id'], $product_ids_on_sale, true ) || in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) || in_array( $cart_item['data']->get_parent(), $product_ids_on_sale, true ) ) { - $valid_for_cart = false; - } - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_VALID_SALE_ITEMS; - } - } - - // Exclude Categories - if ( sizeof( $this->exclude_product_categories ) > 0 ) { - $valid_for_cart = true; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - - $product_cats = wp_get_post_terms( $cart_item['product_id'], 'product_cat', array( "fields" => "ids" ) ); - - if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) - $valid_for_cart = false; - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_APPLICABLE; - } - } - } - - $valid = apply_filters( 'woocommerce_coupon_is_valid', $valid, $this ); - - if ( $valid ) { - return true; - } else { - if ( is_null( $error_code ) ) - $error_code = self::E_WC_COUPON_INVALID_FILTERED; - } - - } else { - $error_code = self::E_WC_COUPON_NOT_EXIST; + private function validate_exists() { + if ( ! $this->get_id() ) { + throw new Exception( self::E_WC_COUPON_NOT_EXIST ); } - - if ( $error_code ) - $this->error_message = $this->get_coupon_error( $error_code ); - - return false; } /** - * Check if a coupon is valid + * Ensure coupon usage limit is valid or throw exception. + * + * @throws Exception + */ + private function validate_usage_limit() { + if ( $this->get_usage_limit() > 0 && $this->get_usage_count() >= $this->get_usage_limit() ) { + throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); + } + } + + /** + * Ensure coupon user usage limit is valid or throw exception. + * + * Per user usage limit - check here if user is logged in (against user IDs). + * Checked again for emails later on in WC_Cart::check_customer_coupons(). + * + * @param int $user_id + * @throws Exception + */ + private function validate_user_usage_limit( $user_id = 0 ) { + if ( empty( $user_id ) ) { + $user_id = get_current_user_id(); + } + if ( $this->get_usage_limit_per_user() > 0 && is_user_logged_in() && $this->get_id() && $this->data_store ) { + $usage_count = $this->data_store->get_usage_by_user_id( $this, $user_id ); + if ( $usage_count >= $this->get_usage_limit_per_user() ) { + throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); + } + } + } + + /** + * Ensure coupon date is valid or throw exception. + * + * @throws Exception + */ + private function validate_expiry_date() { + if ( $this->get_date_expires() && current_time( 'timestamp', true ) > $this->get_date_expires()->getTimestamp() ) { + throw new Exception( $error_code = self::E_WC_COUPON_EXPIRED ); + } + } + + /** + * Ensure coupon amount is valid or throw exception. + * + * @throws Exception + */ + private function validate_minimum_amount() { + if ( $this->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $this->get_minimum_amount() > WC()->cart->get_displayed_subtotal(), $this ) ) { + throw new Exception( self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET ); + } + } + + /** + * Ensure coupon amount is valid or throw exception. + * + * @throws Exception + */ + private function validate_maximum_amount() { + if ( $this->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $this->get_maximum_amount() < WC()->cart->get_displayed_subtotal(), $this ) ) { + throw new Exception( self::E_WC_COUPON_MAX_SPEND_LIMIT_MET ); + } + } + + /** + * Ensure coupon is valid for products in the cart is valid or throw exception. + * + * @throws Exception + */ + private function validate_product_ids() { + if ( sizeof( $this->get_product_ids() ) > 0 ) { + $valid_for_cart = false; + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( in_array( $cart_item['product_id'], $this->get_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_product_ids() ) || in_array( $cart_item['data']->get_parent_id(), $this->get_product_ids() ) ) { + $valid_for_cart = true; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE ); + } + } + } + + /** + * Ensure coupon is valid for product categories in the cart is valid or throw exception. + * + * @throws Exception + */ + private function validate_product_categories() { + if ( sizeof( $this->get_product_categories() ) > 0 ) { + $valid_for_cart = false; + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( $this->get_exclude_sale_items() && $cart_item['data'] && $cart_item['data']->is_on_sale() ) { + continue; + } + $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); + + // If we find an item with a cat in our allowed cat list, the coupon is valid + if ( sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) > 0 ) { + $valid_for_cart = true; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE ); + } + } + } + + /** + * Ensure coupon is valid for sale items in the cart is valid or throw exception. + * + * @throws Exception + */ + private function validate_sale_items() { + if ( $this->get_exclude_sale_items() ) { + $valid_for_cart = false; + $product_ids_on_sale = wc_get_product_ids_on_sale(); + + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( ! empty( $cart_item['variation_id'] ) ) { + if ( ! in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) { + $valid_for_cart = true; + } + } elseif ( ! in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) { + $valid_for_cart = true; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS ); + } + } + } + + /** + * All exclusion rules must pass at the same time for a product coupon to be valid. + */ + private function validate_excluded_items() { + if ( ! WC()->cart->is_empty() && $this->is_type( wc_get_product_coupon_types() ) ) { + $valid = false; + + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( $this->is_valid_for_product( $cart_item['data'], $cart_item ) ) { + $valid = true; + break; + } + } + + if ( ! $valid ) { + throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE ); + } + } + } + + /** + * Cart discounts cannot be added if non-eligble product is found in cart. + */ + private function validate_cart_excluded_items() { + if ( ! $this->is_type( wc_get_product_coupon_types() ) ) { + $this->validate_cart_excluded_product_ids(); + $this->validate_cart_excluded_product_categories(); + } + } + + /** + * Exclude products from cart. + * + * @throws Exception + */ + private function validate_cart_excluded_product_ids() { + // Exclude Products + if ( sizeof( $this->get_excluded_product_ids() ) > 0 ) { + $valid_for_cart = true; + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( in_array( $cart_item['product_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['data']->get_parent_id(), $this->get_excluded_product_ids() ) ) { + $valid_for_cart = false; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_EXCLUDED_PRODUCTS ); + } + } + } + + /** + * Exclude categories from cart. + * + * @throws Exception + */ + private function validate_cart_excluded_product_categories() { + if ( sizeof( $this->get_excluded_product_categories() ) > 0 ) { + $valid_for_cart = true; + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( $this->get_exclude_sale_items() && $cart_item['data'] && $cart_item['data']->is_on_sale() ) { + continue; + } + $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); + + if ( sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) { + $valid_for_cart = false; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_EXCLUDED_CATEGORIES ); + } + } + } + + /** + * Check if a coupon is valid. + * + * @return boolean validity + * @throws Exception + */ + public function is_valid() { + try { + $this->validate_exists(); + $this->validate_usage_limit(); + $this->validate_user_usage_limit(); + $this->validate_expiry_date(); + $this->validate_minimum_amount(); + $this->validate_maximum_amount(); + $this->validate_product_ids(); + $this->validate_product_categories(); + $this->validate_sale_items(); + $this->validate_excluded_items(); + $this->validate_cart_excluded_items(); + + if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $this ) ) { + throw new Exception( self::E_WC_COUPON_INVALID_FILTERED ); + } + } catch ( Exception $e ) { + $this->error_message = $this->get_coupon_error( $e->getMessage() ); + return false; + } + + return true; + } + + /** + * Check if a coupon is valid. * * @return bool */ public function is_valid_for_cart() { - $valid = $this->type != 'fixed_cart' && $this->type != 'percent' ? false : true; - return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $valid, $this ); + return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( wc_get_cart_coupon_types() ), $this ); } /** - * Check if a coupon is valid for a product - * - * @param WC_Product $product - * @return boolean + * Check if a coupon is valid for a product. + * + * @param WC_Product $product + * @param array $values + * + * @return bool */ - public function is_valid_for_product( $product ) { - if ( $this->type != 'fixed_product' && $this->type != 'percent_product' ) - return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this ); + public function is_valid_for_product( $product, $values = array() ) { + if ( ! $this->is_type( wc_get_product_coupon_types() ) ) { + return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values ); + } $valid = false; - $product_cats = wp_get_post_terms( $product->id, 'product_cat', array( "fields" => "ids" ) ); + $product_cats = wc_get_product_cat_ids( $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id() ); + $product_ids = array( $product->get_id(), $product->get_parent_id() ); // Specific products get the discount - if ( sizeof( $this->product_ids ) > 0 ) { - - if ( in_array( $product->id, $this->product_ids ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $this->product_ids ) ) || in_array( $product->get_parent(), $this->product_ids ) ) - $valid = true; - - // Category discounts - } elseif ( sizeof( $this->product_categories ) > 0 ) { - - if ( sizeof( array_intersect( $product_cats, $this->product_categories ) ) > 0 ) - $valid = true; - - } else { - // No product ids - all items discounted + if ( sizeof( $this->get_product_ids() ) && sizeof( array_intersect( $product_ids, $this->get_product_ids() ) ) ) { $valid = true; } - // Specific product ID's excluded from the discount - if ( sizeof( $this->exclude_product_ids ) > 0 ) - if ( in_array( $product->id, $this->exclude_product_ids ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $this->exclude_product_ids ) ) || in_array( $product->get_parent(), $this->exclude_product_ids ) ) - $valid = false; + // Category discounts + if ( sizeof( $this->get_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) ) { + $valid = true; + } + + // No product ids - all items discounted + if ( ! sizeof( $this->get_product_ids() ) && ! sizeof( $this->get_product_categories() ) ) { + $valid = true; + } + + // Specific product IDs excluded from the discount + if ( sizeof( $this->get_excluded_product_ids() ) && sizeof( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) { + $valid = false; + } // Specific categories excluded from the discount - if ( sizeof( $this->exclude_product_categories ) > 0 ) - if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) - $valid = false; + if ( sizeof( $this->get_excluded_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) { + $valid = false; + } // Sale Items excluded from discount - if ( $this->exclude_sale_items == 'yes' ) { + if ( $this->get_exclude_sale_items() ) { $product_ids_on_sale = wc_get_product_ids_on_sale(); - if ( in_array( $product->id, $product_ids_on_sale, true ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $product_ids_on_sale, true ) ) || in_array( $product->get_parent(), $product_ids_on_sale, true ) ) + if ( in_array( $product->get_id(), $product_ids_on_sale, true ) ) { $valid = false; + } } - return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this ); + return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values ); } /** - * Get discount amount for a cart item - * - * @param float $discounting_amount Amount the coupon is being applied to - * @param array|null $cart_item Cart item being discounted if applicable - * @param boolean $single True if discounting a single qty item, false if its the line - * @return float Amount this coupon has discounted - */ - public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) { - $discount = 0; - - if ( $this->type == 'fixed_product') { - - $discount = $discounting_amount < $this->amount ? $discounting_amount : $this->amount; - - // If dealing with a line and not a single item, we need to multiple fixed discount by cart item qty. - if ( ! $single && ! is_null( $cart_item ) ) { - // Discount for the line. - $discount = $discount * $cart_item['quantity']; - } - - } elseif ( $this->type == 'percent_product' || $this->type == 'percent' ) { - - $discount = round( ( $discounting_amount / 100 ) * $this->amount, WC()->cart->dp ); - - } elseif ( $this->type == 'fixed_cart' ) { - if ( ! is_null( $cart_item ) ) { - /** - * This is the most complex discount - we need to divide the discount between rows based on their price in - * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows - * with no price (free) don't get discounted. - * - * Get item discount by dividing item cost by subtotal to get a % - */ - $discount_percent = 0; - - if ( WC()->cart->subtotal_ex_tax ) { - $discount_percent = ( $cart_item['data']->get_price_excluding_tax() * $cart_item['quantity'] ) / WC()->cart->subtotal_ex_tax; - } - - $discount = min( ( $this->amount * $discount_percent ) / $cart_item['quantity'], $discounting_amount ); - } else { - $discount = min( $this->amount, $discounting_amount ); - } - } - - // Handle the limit_usage_to_x_items option - if ( in_array( $this->type, array( 'percent_product', 'fixed_product' ) ) && ! is_null( $cart_item ) ) { - $qty = empty( $this->limit_usage_to_x_items ) ? $cart_item['quantity'] : min( $this->limit_usage_to_x_items, $cart_item['quantity'] ); - - if ( $single ) { - $discount = ( $discount * $qty ) / $cart_item['quantity']; - } else { - $discount = ( $discount / $cart_item['quantity'] ) * $qty; - } - } - - return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this ); - } - - /** - * Converts one of the WC_Coupon message/error codes to a message string and + * Converts one of the WC_Coupon message/error codes to a message string and. * displays the message/error. * - * @access public * @param int $msg_code Message/error code. - * @return void */ public function add_coupon_message( $msg_code ) { + $msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code ); - if ( $msg_code < 200 ) - wc_add_notice( $this->get_coupon_error( $msg_code ), 'error' ); - else - wc_add_notice( $this->get_coupon_message( $msg_code ) ); + if ( ! $msg ) { + return; + } + + if ( $msg_code < 200 ) { + wc_add_notice( $msg, 'error' ); + } else { + wc_add_notice( $msg ); + } } /** - * Map one of the WC_Coupon message codes to a message string + * Map one of the WC_Coupon message codes to a message string. * - * @access public - * @param mixed $msg_code + * @param integer $msg_code * @return string| Message/error string */ public function get_coupon_message( $msg_code ) { - switch ( $msg_code ) { case self::WC_COUPON_SUCCESS : $msg = __( 'Coupon code applied successfully.', 'woocommerce' ); @@ -600,37 +1121,38 @@ class WC_Coupon { $msg = ''; break; } - return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this ); } /** - * Map one of the WC_Coupon error codes to a message string + * Map one of the WC_Coupon error codes to a message string. * - * @access public * @param int $err_code Message/error code. * @return string| Message/error string */ public function get_coupon_error( $err_code ) { - switch ( $err_code ) { case self::E_WC_COUPON_INVALID_FILTERED: $err = __( 'Coupon is not valid.', 'woocommerce' ); break; case self::E_WC_COUPON_NOT_EXIST: - $err = __( 'Coupon does not exist!', 'woocommerce' ); + /* translators: %s: coupon code */ + $err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $this->get_code() ); break; case self::E_WC_COUPON_INVALID_REMOVED: - $err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), $this->code ); + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), $this->get_code() ); break; case self::E_WC_COUPON_NOT_YOURS_REMOVED: - $err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), $this->code ); + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), $this->get_code() ); break; case self::E_WC_COUPON_ALREADY_APPLIED: $err = __( 'Coupon code already applied!', 'woocommerce' ); break; case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY: - $err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $this->code ); + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $this->get_code() ); break; case self::E_WC_COUPON_USAGE_LIMIT_REACHED: $err = __( 'Coupon usage limit has been reached.', 'woocommerce' ); @@ -639,11 +1161,50 @@ class WC_Coupon { $err = __( 'This coupon has expired.', 'woocommerce' ); break; case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET: - $err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->minimum_amount ) ); + /* translators: %s: coupon minimum amount */ + $err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_minimum_amount() ) ); + break; + case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET: + /* translators: %s: coupon maximum amount */ + $err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_maximum_amount() ) ); break; case self::E_WC_COUPON_NOT_APPLICABLE: $err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' ); break; + case self::E_WC_COUPON_EXCLUDED_PRODUCTS: + // Store excluded products that are in cart in $products + $products = array(); + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( in_array( $cart_item['product_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['data']->get_parent_id(), $this->get_excluded_product_ids() ) ) { + $products[] = $cart_item['data']->get_name(); + } + } + } + + /* translators: %s: products list */ + $err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ); + break; + case self::E_WC_COUPON_EXCLUDED_CATEGORIES: + // Store excluded categories that are in cart in $categories + $categories = array(); + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); + + if ( sizeof( $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) { + + foreach ( $intersect as $cat_id ) { + $cat = get_term( $cat_id, 'product_cat' ); + $categories[] = $cat->name; + } + } + } + } + + /* translators: %s: categories list */ + $err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ); + break; case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS: $err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ); break; @@ -651,21 +1212,18 @@ class WC_Coupon { $err = ''; break; } - return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this ); } /** - * Map one of the WC_Coupon error codes to an error string + * Map one of the WC_Coupon error codes to an error string. * No coupon instance will be available where a coupon does not exist, * so this static method exists. * - * @access public * @param int $err_code Error code * @return string| Error string */ public static function get_generic_coupon_error( $err_code ) { - switch ( $err_code ) { case self::E_WC_COUPON_NOT_EXIST: $err = __( 'Coupon does not exist!', 'woocommerce' ); @@ -677,9 +1235,7 @@ class WC_Coupon { $err = ''; break; } - // When using this static method, there is no $this to pass to filter return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null ); } - } diff --git a/includes/class-wc-customer-download.php b/includes/class-wc-customer-download.php new file mode 100644 index 00000000000..a2a40255acb --- /dev/null +++ b/includes/class-wc-customer-download.php @@ -0,0 +1,362 @@ + '', + 'product_id' => 0, + 'user_id' => 0, + 'user_email' => '', + 'order_id' => 0, + 'order_key' => '', + 'downloads_remaining' => '', + 'access_granted' => null, + 'access_expires' => null, + 'download_count' => 0, + ); + + /** + * Constructor. + * + * @param int|object|array $download + */ + public function __construct( $download = 0 ) { + parent::__construct( $download ); + + if ( is_numeric( $download ) && $download > 0 ) { + $this->set_id( $download ); + } elseif ( $download instanceof self ) { + $this->set_id( $download->get_id() ); + } elseif ( is_object( $download ) && ! empty( $download->permission_id ) ) { + $this->set_id( $download->permission_id ); + $this->set_props( (array) $download ); + $this->set_object_read( true ); + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( 'customer-download' ); + + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get download id. + * + * @param string $context + * @return string + */ + public function get_download_id( $context = 'view' ) { + return $this->get_prop( 'download_id', $context ); + } + + /** + * Get product id. + * + * @param string $context + * @return integer + */ + public function get_product_id( $context = 'view' ) { + return $this->get_prop( 'product_id', $context ); + } + + /** + * Get user id. + * + * @param string $context + * @return integer + */ + public function get_user_id( $context = 'view' ) { + return $this->get_prop( 'user_id', $context ); + } + + /** + * Get user_email. + * + * @param string $context + * @return string + */ + public function get_user_email( $context = 'view' ) { + return $this->get_prop( 'user_email', $context ); + } + + /** + * Get order_id. + * + * @param string $context + * @return integer + */ + public function get_order_id( $context = 'view' ) { + return $this->get_prop( 'order_id', $context ); + } + + /** + * Get order_key. + * + * @param string $context + * @return string + */ + public function get_order_key( $context = 'view' ) { + return $this->get_prop( 'order_key', $context ); + } + + /** + * Get downloads_remaining. + * + * @param string $context + * @return integer|string + */ + public function get_downloads_remaining( $context = 'view' ) { + return $this->get_prop( 'downloads_remaining', $context ); + } + + /** + * Get access_granted. + * + * @param string $context + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_access_granted( $context = 'view' ) { + return $this->get_prop( 'access_granted', $context ); + } + + /** + * Get access_expires. + * + * @param string $context + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_access_expires( $context = 'view' ) { + return $this->get_prop( 'access_expires', $context ); + } + + /** + * Get download_count. + * + * @param string $context + * @return integer + */ + public function get_download_count( $context = 'view' ) { + return $this->get_prop( 'download_count', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set download id. + * + * @param string $value + */ + public function set_download_id( $value ) { + $this->set_prop( 'download_id', $value ); + } + /** + * Set product id. + * + * @param int $value + */ + public function set_product_id( $value ) { + $this->set_prop( 'product_id', absint( $value ) ); + } + + /** + * Get user id. + * + * @param int $value + */ + public function set_user_id( $value ) { + $this->set_prop( 'user_id', absint( $value ) ); + } + + /** + * Get user_email. + * + * @param int $value + */ + public function set_user_email( $value ) { + $this->set_prop( 'user_email', sanitize_email( $value ) ); + } + + /** + * Get order_id. + * + * @param int $value + */ + public function set_order_id( $value ) { + $this->set_prop( 'order_id', absint( $value ) ); + } + + /** + * Get order_key. + * + * @param string $value + */ + public function set_order_key( $value ) { + $this->set_prop( 'order_key', $value ); + } + + /** + * Get downloads_remaining. + * + * @param integer|string $value + */ + public function set_downloads_remaining( $value ) { + $this->set_prop( 'downloads_remaining', '' === $value ? '' : absint( $value ) ); + } + + /** + * Get access_granted. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_access_granted( $date = null ) { + $this->set_date_prop( 'access_granted', $date ); + } + + /** + * Get access_expires. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_access_expires( $date = null ) { + $this->set_date_prop( 'access_expires', $date ); + } + + /** + * Get download_count. + * + * @param int $value + */ + public function set_download_count( $value ) { + $this->set_prop( 'download_count', absint( $value ) ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + */ + + /** + * Save data to the database. + * @since 3.0.0 + * @return int Item ID + */ + public function save() { + if ( $this->data_store ) { + // Trigger action before saving to the DB. Use a pointer to adjust object props before save. + do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); + + if ( $this->get_id() ) { + $this->data_store->update( $this ); + } else { + $this->data_store->create( $this ); + } + } + return $this->get_id(); + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * offsetGet + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + } + + /** + * offsetSet + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + if ( is_callable( array( $this, "set_$offset" ) ) ) { + $this->{"set_$offset"}( $value ); + } + } + + /** + * offsetUnset + * @param string $offset + */ + public function offsetUnset( $offset ) { + if ( is_callable( array( $this, "set_$offset" ) ) ) { + $this->{"set_$offset"}( '' ); + } + } + + /** + * offsetExists + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + return in_array( $offset, array_keys( $this->data ) ); + } + + /** + * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past. + * + * @param string $key Key name. + * @return bool + */ + public function __isset( $key ) { + return in_array( $offset, array_keys( $this->data ) ); + } + + /** + * Magic __get method for backwards compatibility. Maps legacy vars to new getters. + * + * @param string $key Key name. + * @return mixed + */ + public function __get( $key ) { + if ( is_callable( array( $this, "get_$key" ) ) ) { + return $this->{"get_$key"}( '' ); + } + } +} diff --git a/includes/class-wc-customer.php b/includes/class-wc-customer.php index 05089f281a6..e12916886a6 100644 --- a/includes/class-wc-customer.php +++ b/includes/class-wc-customer.php @@ -1,558 +1,1086 @@ _data = WC()->session->get( 'customer' ); + protected $data = array( + 'date_created' => null, + 'date_modified' => null, + 'email' => '', + 'first_name' => '', + 'last_name' => '', + 'display_name' => '', + 'role' => 'customer', + 'username' => '', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + ), + 'is_paying_customer' => false, + ); - if ( empty( $this->_data ) ) { + /** + * Stores a password if this needs to be changed. Write-only and hidden from _data. + * + * @var string + */ + protected $password = ''; - $default = apply_filters( 'woocommerce_customer_default_location', get_option( 'woocommerce_default_country' ) ); + /** + * Stores if user is VAT exempt for this session. + * + * @var string + */ + protected $is_vat_exempt = false; - if ( strstr( $default, ':' ) ) { - list( $country, $state ) = explode( ':', $default ); - } else { - $country = $default; - $state = ''; - } + /** + * Stores if user has calculated shipping in this session. + * + * @var string + */ + protected $calculated_shipping = false; - $this->_data = array( - 'country' => esc_html( $country ), - 'state' => '', - 'postcode' => '', - 'city' => '', - 'address' => '', - 'address_2' => '', - 'shipping_country' => esc_html( $country ), - 'shipping_state' => '', - 'shipping_postcode' => '', - 'shipping_city' => '', - 'shipping_address' => '', - 'shipping_address_2' => '', - 'is_vat_exempt' => false, - 'calculated_shipping' => false - ); + /** + * Load customer data based on how WC_Customer is called. + * + * If $customer is 'new', you can build a new WC_Customer object. If it's empty, some + * data will be pulled from the session for the current user/customer. + * + * @param WC_Customer|int $data Customer ID or data. + * @param bool $is_session True if this is the customer session + * @throws Exception if customer cannot be read/found and $data is set. + */ + public function __construct( $data = 0, $is_session = false ) { + parent::__construct( $data ); + + if ( $data instanceof WC_Customer ) { + $this->set_id( absint( $data->get_id() ) ); + } elseif ( is_numeric( $data ) ) { + $this->set_id( $data ); } - // When leaving or ending page load, store data - add_action( 'shutdown', array( $this, 'save_data' ), 10 ); - } + $this->data_store = WC_Data_Store::load( 'customer' ); - /** - * save_data function. - * - * @access public - */ - public function save_data() { - if ( $this->_changed ) { - WC()->session->set( 'customer', $this->_data ); + // If we have an ID, load the user from the DB. + if ( $this->get_id() ) { + try { + $this->data_store->read( $this ); + } catch ( Exception $e ) { + $this->set_id( 0 ); + $this->set_object_read( true ); + } + } else { + $this->set_object_read( true ); + } + + // If this is a session, set or change the data store to sessions. Changes do not persist in the database. + if ( $is_session ) { + $this->data_store = WC_Data_Store::load( 'customer-session' ); + $this->data_store->read( $this ); } } /** - * __set function. - * @access public - * @param mixed $property - * @return bool - */ - public function __isset( $property ) { - return isset( $this->_data[ $property ] ); - } - - /** - * __get function. - * - * @access public - * @param string $property - * @return string - */ - public function __get( $property ) { - return isset( $this->_data[ $property ] ) ? $this->_data[ $property ] : ''; - } - - /** - * __set function. - * - * @access public - * @param mixed $property - * @param mixed $value - */ - public function __set( $property, $value ) { - $this->_data[ $property ] = $value; - $this->_changed = true; - } - - /** - * has_calculated_shipping function. + * Prefix for action and filter hooks on data. * - * @access public - * @return bool + * @since 3.0.0 + * @return string */ - public function has_calculated_shipping() { - return ( ! empty( $this->calculated_shipping ) ) ? true : false; + protected function get_hook_prefix() { + return 'woocommerce_customer_get_'; } /** - * Set customer address to match shop base address. + * Delete a customer and reassign posts.. * - * @access public - */ - public function set_to_base() { - $default = apply_filters( 'woocommerce_customer_default_location', get_option('woocommerce_default_country') ); - if ( strstr( $default, ':' ) ) { - list( $country, $state ) = explode( ':', $default ); - } else { - $country = $default; - $state = ''; - } - $this->country = $country; - $this->state = $state; - $this->postcode = ''; - $this->city = ''; - } - - /** - * Set customer shipping address to base address. - * - * @access public - */ - public function set_shipping_to_base() { - $default = get_option('woocommerce_default_country'); - if ( strstr( $default, ':' ) ) { - list( $country, $state ) = explode( ':', $default ); - } else { - $country = $default; - $state = ''; - } - $this->shipping_country = $country; - $this->shipping_state = $state; - $this->shipping_postcode = ''; - $this->shipping_city = ''; - } - - /** - * Is customer outside base country (for tax purposes)? - * - * @access public + * @param int $reassign Reassign posts and links to new User ID. + * @since 3.0.0 * @return bool */ - public function is_customer_outside_base() { - list( $country, $state, $postcode, $city ) = $this->get_taxable_address(); - - if ( $country ) { - - $default = get_option('woocommerce_default_country'); - - if ( strstr( $default, ':' ) ) { - list( $default_country, $default_state ) = explode( ':', $default ); - } else { - $default_country = $default; - $default_state = ''; - } - - if ( $default_country !== $country ) return true; - if ( $default_state && $default_state !== $state ) return true; - + public function delete_and_reassign( $reassign = null ) { + if ( $this->data_store ) { + $this->data_store->delete( $this, array( 'force_delete' => true, 'reassign' => $reassign ) ); + $this->set_id( 0 ); + return true; } return false; } /** - * Is the user a paying customer? + * Is customer outside base country (for tax purposes)? * - * @access public * @return bool */ - function is_paying_customer( $user_id ) { - return '1' === get_user_meta( $user_id, 'paying_customer', true ); + public function is_customer_outside_base() { + list( $country, $state ) = $this->get_taxable_address(); + if ( $country ) { + $default = wc_get_base_location(); + if ( $default['country'] !== $country ) { + return true; + } + if ( $default['state'] && $default['state'] !== $state ) { + return true; + } + } + return false; } /** - * Is customer VAT exempt? + * Return this customer's avatar. * - * @access public - * @return bool - */ - public function is_vat_exempt() { - return ( ! empty( $this->is_vat_exempt ) ) ? true : false; - } - - /** - * Gets the state from the current session. - * - * @access public + * @since 3.0.0 * @return string */ - public function get_state() { - return $this->state; + public function get_avatar_url() { + return get_avatar_url( $this->get_email() ); } /** - * Gets the country from the current session - * - * @access public - * @return string - */ - public function get_country() { - return $this->country; - } - - /** - * Gets the postcode from the current session. - * - * @access public - * @return string - */ - public function get_postcode() { - return empty( $this->postcode ) ? '' : wc_format_postcode( $this->postcode, $this->get_country() ); - } - - /** - * Get the city from the current session. - * - * @access public - * @return string - */ - public function get_city() { - return $this->city; - } - - /** - * Gets the address from the current session. - * - * @access public - * @return string - */ - public function get_address() { - return $this->address; - } - - /** - * Gets the address_2 from the current session. - * - * @access public - * @return string - */ - public function get_address_2() { - return $this->address_2; - } - - /** - * Gets the state from the current session. - * - * @access public - * @return string - */ - public function get_shipping_state() { - return $this->shipping_state; - } - - - /** - * Gets the country from the current session. - * - * @access public - * @return string - */ - public function get_shipping_country() { - return $this->shipping_country; - } - - - /** - * Gets the postcode from the current session. - * - * @access public - * @return string - */ - public function get_shipping_postcode() { - return empty( $this->shipping_postcode ) ? '' : wc_format_postcode( $this->shipping_postcode, $this->get_shipping_country() ); - } - - - /** - * Gets the city from the current session. - * - * @access public - * @return string - */ - public function get_shipping_city() { - return $this->shipping_city; - } - - /** - * Gets the address from the current session. - * - * @access public - * @return string - */ - public function get_shipping_address() { - return $this->shipping_address; - } - - /** - * Gets the address_2 from the current session. - * - * @access public - * @return string - */ - public function get_shipping_address_2() { - return $this->shipping_address_2; - } - - /** - * get_taxable_address function. - * - * @access public + * Get taxable address. * @return array */ public function get_taxable_address() { $tax_based_on = get_option( 'woocommerce_tax_based_on' ); // Check shipping method at this point to see if we need special handling - if ( apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) == true && sizeof( array_intersect( WC()->session->get( 'chosen_shipping_methods', array( get_option( 'woocommerce_default_shipping_method' ) ) ), apply_filters( 'woocommerce_local_pickup_methods', array( 'local_pickup' ) ) ) ) > 0 ) { + if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && sizeof( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) { $tax_based_on = 'base'; } - if ( $tax_based_on == 'base' ) { - - $default = get_option( 'woocommerce_default_country' ); - if ( strstr( $default, ':' ) ) { - list( $country, $state ) = explode( ':', $default ); - } else { - $country = $default; - $state = ''; - } - - $postcode = ''; - $city = ''; - - } elseif ( $tax_based_on == 'billing' ) { - - $country = $this->get_country(); - $state = $this->get_state(); - $postcode = $this->get_postcode(); - $city = $this->get_city(); - + if ( 'base' === $tax_based_on ) { + $country = WC()->countries->get_base_country(); + $state = WC()->countries->get_base_state(); + $postcode = WC()->countries->get_base_postcode(); + $city = WC()->countries->get_base_city(); + } elseif ( 'billing' === $tax_based_on ) { + $country = $this->get_billing_country(); + $state = $this->get_billing_state(); + $postcode = $this->get_billing_postcode(); + $city = $this->get_billing_city(); } else { - - $country = $this->get_shipping_country(); - $state = $this->get_shipping_state(); - $postcode = $this->get_shipping_postcode(); - $city = $this->get_shipping_city(); - + $country = $this->get_shipping_country(); + $state = $this->get_shipping_state(); + $postcode = $this->get_shipping_postcode(); + $city = $this->get_shipping_city(); } return apply_filters( 'woocommerce_customer_taxable_address', array( $country, $state, $postcode, $city ) ); } - /** - * Sets session data for the location. + * Gets a customer's downloadable products. * - * @access public - * @param mixed $country - * @param mixed $state - * @param string $postcode (default: '') - * @param string $city (default: '') - */ - public function set_location( $country, $state, $postcode = '', $city = '' ) { - $this->country = $country; - $this->state = $state; - $this->postcode = $postcode; - $this->city = $city; - } - - /** - * Sets session data for the country. - * - * @access public - * @param mixed $country - */ - public function set_country( $country ) { - $this->country = $country; - } - - /** - * Sets session data for the state. - * - * @access public - * @param mixed $state - */ - public function set_state( $state ) { - $this->state = $state; - } - - /** - * Sets session data for the postcode. - * - * @access public - * @param mixed $postcode - */ - public function set_postcode( $postcode ) { - $this->postcode = $postcode; - } - - /** - * Sets session data for the city. - * - * @access public - * @param mixed $postcode - */ - public function set_city( $city ) { - $this->city = $city; - } - - /** - * Sets session data for the address. - * - * @access public - * @param mixed $address - */ - public function set_address( $address ) { - $this->address = $address; - } - - /** - * Sets session data for the address_2. - * - * @access public - * @param mixed $address_2 - */ - public function set_address_2( $address_2 ) { - $this->address_2 = $address_2; - } - - /** - * Sets session data for the location. - * - * @access public - * @param mixed $country - * @param string $state (default: '') - * @param string $postcode (default: '') - * @param string $city (default: '') - */ - public function set_shipping_location( $country, $state = '', $postcode = '', $city = '' ) { - $this->shipping_country = $country; - $this->shipping_state = $state; - $this->shipping_postcode = $postcode; - $this->shipping_city = $city; - } - - /** - * Sets session data for the country. - * - * @access public - * @param string $country - */ - public function set_shipping_country( $country ) { - $this->shipping_country = $country; - } - - /** - * Sets session data for the state. - * - * @access public - * @param string $state - */ - public function set_shipping_state( $state ) { - $this->shipping_state = $state; - } - - /** - * Sets session data for the postcode. - * - * @access public - * @param string $postcode - */ - public function set_shipping_postcode( $postcode ) { - $this->shipping_postcode = $postcode; - } - - /** - * Sets session data for the city. - * - * @access public - * @param string $postcode - */ - public function set_shipping_city( $city ) { - $this->shipping_city = $city; - } - - /** - * Sets session data for the address. - * - * @access public - * @param string $address - */ - public function set_shipping_address( $address ) { - $this->shipping_address = $address; - } - - /** - * Sets session data for the address_2. - * - * @access public - * @param string $address_2 - */ - public function set_shipping_address_2( $address_2 ) { - $this->shipping_address_2 = $address_2; - } - - /** - * Sets session data for the tax exemption. - * - * @access public - * @param bool $is_vat_exempt - */ - public function set_is_vat_exempt( $is_vat_exempt ) { - $this->is_vat_exempt = $is_vat_exempt; - } - - /** - * calculated_shipping function. - * - * @access public - * @param mixed $calculated - */ - public function calculated_shipping( $calculated = true ) { - $this->calculated_shipping = $calculated; - } - - /** - * Gets a user's downloadable products if they are logged in. - * - * @access public * @return array Array of downloadable products */ public function get_downloadable_products() { $downloads = array(); - - if ( is_user_logged_in() ) { - $downloads = wc_get_customer_available_downloads( get_current_user_id() ); + if ( $this->get_id() ) { + $downloads = wc_get_customer_available_downloads( $this->get_id() ); } - return apply_filters( 'woocommerce_customer_get_downloadable_products', $downloads ); } + + /** + * Is customer VAT exempt? + * + * @return bool + */ + public function is_vat_exempt() { + return $this->get_is_vat_exempt(); + } + + /** + * Has calculated shipping? + * + * @return bool + */ + public function has_calculated_shipping() { + return $this->get_calculated_shipping(); + } + + /** + * Get if customer is VAT exempt? + * + * @since 3.0.0 + * @return bool + */ + public function get_is_vat_exempt() { + return $this->is_vat_exempt; + } + + /** + * Get password (only used when updating the user object). + * + * @return string + */ + public function get_password() { + return $this->password; + } + + /** + * Has customer calculated shipping? + * + * @return bool + */ + public function get_calculated_shipping() { + return $this->calculated_shipping; + } + + /** + * Set if customer has tax exemption. + * + * @param bool $is_vat_exempt + */ + public function set_is_vat_exempt( $is_vat_exempt ) { + $this->is_vat_exempt = (bool) $is_vat_exempt; + } + + /** + * Calculated shipping? + * + * @param boolean $calculated + */ + public function set_calculated_shipping( $calculated = true ) { + $this->calculated_shipping = (bool) $calculated; + } + + /** + * Set customer's password. + * + * @since 3.0.0 + * @param string $password + * @throws WC_Data_Exception + */ + public function set_password( $password ) { + $this->password = wc_clean( $password ); + } + + /** + * Gets the customers last order. + * + * @param WC_Customer + * @return WC_Order|false + */ + public function get_last_order() { + return $this->data_store->get_last_order( $this ); + } + + /** + * Return the number of orders this customer has. + * + * @param WC_Customer + * @return integer + */ + public function get_order_count() { + return $this->data_store->get_order_count( $this ); + } + + /** + * Return how much money this customer has spent. + * + * @param WC_Customer + * @return float + */ + public function get_total_spent() { + return $this->data_store->get_total_spent( $this ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Return the customer's username. + * + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_username( $context = 'view' ) { + return $this->get_prop( 'username', $context ); + } + + /** + * Return the customer's email. + * + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_email( $context = 'view' ) { + return $this->get_prop( 'email', $context ); + } + + /** + * Return customer's first name. + * + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_first_name( $context = 'view' ) { + return $this->get_prop( 'first_name', $context ); + } + + /** + * Return customer's last name. + * + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_last_name( $context = 'view' ) { + return $this->get_prop( 'last_name', $context ); + } + + /** + * Return customer's display name. + * + * @since 3.1.0 + * @param string $context + * @return string + */ + public function get_display_name( $context = 'view' ) { + return $this->get_prop( 'display_name', $context ); + } + + /** + * Return customer's user role. + * + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_role( $context = 'view' ) { + return $this->get_prop( 'role', $context ); + } + + /** + * Return the date this customer was created. + * + * @since 3.0.0 + * @param string $context + * @return WC_DateTime|null object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Return the date this customer was last updated. + * + * @since 3.0.0 + * @param string $context + * @return WC_DateTime|null object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Gets a prop for a getter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to get. + * @param string $address billing or shipping. + * @param string $context What the value is for. Valid values are view and edit. + * @return mixed + */ + protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { + $value = null; + + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); + } + } + return $value; + } + + /** + * Get billing_first_name. + * + * @param string $context + * @return string + */ + public function get_billing_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'billing', $context ); + } + + /** + * Get billing_last_name. + * + * @param string $context + * @return string + */ + public function get_billing_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'billing', $context ); + } + + /** + * Get billing_company. + * + * @param string $context + * @return string + */ + public function get_billing_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'billing', $context ); + } + + /** + * Get billing_address_1. + * + * @param string $context + * @return string + */ + public function get_billing_address( $context = 'view' ) { + return $this->get_billing_address_1( $context ); + } + + /** + * Get billing_address_1. + * + * @param string $context + * @return string + */ + public function get_billing_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'billing', $context ); + } + + /** + * Get billing_address_2. + * + * @param string $context + * @return string $value + */ + public function get_billing_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'billing', $context ); + } + + /** + * Get billing_city. + * + * @param string $context + * @return string $value + */ + public function get_billing_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'billing', $context ); + } + + /** + * Get billing_state. + * + * @param string $context + * @return string + */ + public function get_billing_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'billing', $context ); + } + + /** + * Get billing_postcode. + * + * @param string $context + * @return string + */ + public function get_billing_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'billing', $context ); + } + + /** + * Get billing_country. + * + * @param string $context + * @return string + */ + public function get_billing_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'billing', $context ); + } + + /** + * Get billing_email. + * + * @param string $context + * @return string + */ + public function get_billing_email( $context = 'view' ) { + return $this->get_address_prop( 'email', 'billing', $context ); + } + + /** + * Get billing_phone. + * + * @param string $context + * @return string + */ + public function get_billing_phone( $context = 'view' ) { + return $this->get_address_prop( 'phone', 'billing', $context ); + } + + /** + * Get shipping_first_name. + * + * @param string $context + * @return string + */ + public function get_shipping_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'shipping', $context ); + } + + /** + * Get shipping_last_name. + * + * @param string $context + * @return string + */ + public function get_shipping_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'shipping', $context ); + } + + /** + * Get shipping_company. + * + * @param string $context + * @return string + */ + public function get_shipping_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'shipping', $context ); + } + + /** + * Get shipping_address_1. + * + * @param string $context + * @return string + */ + public function get_shipping_address( $context = 'view' ) { + return $this->get_shipping_address_1( $context ); + } + + /** + * Get shipping_address_1. + * + * @param string $context + * @return string + */ + public function get_shipping_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'shipping', $context ); + } + + /** + * Get shipping_address_2. + * + * @param string $context + * @return string + */ + public function get_shipping_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'shipping', $context ); + } + + /** + * Get shipping_city. + * + * @param string $context + * @return string + */ + public function get_shipping_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'shipping', $context ); + } + + /** + * Get shipping_state. + * + * @param string $context + * @return string + */ + public function get_shipping_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'shipping', $context ); + } + + /** + * Get shipping_postcode. + * + * @param string $context + * @return string + */ + public function get_shipping_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'shipping', $context ); + } + + /** + * Get shipping_country. + * + * @param string $context + * @return string + */ + public function get_shipping_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'shipping', $context ); + } + + /** + * Is the user a paying customer? + * + * @since 3.0.0 + * @param string $context + * @return bool + */ + function get_is_paying_customer( $context = 'view' ) { + return $this->get_prop( 'is_paying_customer', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set customer's username. + * + * @since 3.0.0 + * @param string $username + * @throws WC_Data_Exception + */ + public function set_username( $username ) { + $this->set_prop( 'username', $username ); + } + + /** + * Set customer's email. + * + * @since 3.0.0 + * @param string $value + * @throws WC_Data_Exception + */ + public function set_email( $value ) { + if ( $value && ! is_email( $value ) ) { + $this->error( 'customer_invalid_email', __( 'Invalid email address', 'woocommerce' ) ); + } + $this->set_prop( 'email', sanitize_email( $value ) ); + } + + /** + * Set customer's first name. + * + * @since 3.0.0 + * @param string $first_name + * @throws WC_Data_Exception + */ + public function set_first_name( $first_name ) { + $this->set_prop( 'first_name', $first_name ); + } + + /** + * Set customer's last name. + * + * @since 3.0.0 + * @param string $last_name + * @throws WC_Data_Exception + */ + public function set_last_name( $last_name ) { + $this->set_prop( 'last_name', $last_name ); + } + + /** + * Set customer's display name. + * + * @since 3.1.0 + * @param string $display_name + * @throws WC_Data_Exception + */ + public function set_display_name( $display_name ) { + $this->set_prop( 'display_name', $display_name ); + } + + /** + * Set customer's user role(s). + * + * @since 3.0.0 + * @param mixed $role + * @throws WC_Data_Exception + */ + public function set_role( $role ) { + global $wp_roles; + + if ( $role && ! empty( $wp_roles->roles ) && ! in_array( $role, array_keys( $wp_roles->roles ) ) ) { + $this->error( 'customer_invalid_role', __( 'Invalid role', 'woocommerce' ) ); + } + $this->set_prop( 'role', $role ); + } + + /** + * Set the date this customer was last updated. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + * @throws WC_Data_Exception + */ + public function set_date_created( $date = null ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set the date this customer was last updated. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + * @throws WC_Data_Exception + */ + public function set_date_modified( $date = null ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set customer address to match shop base address. + * + * @since 3.0.0 + * @throws WC_Data_Exception + */ + public function set_billing_address_to_base() { + $base = wc_get_customer_default_location(); + $this->set_billing_location( $base['country'], $base['state'], '', '' ); + } + + /** + * Set customer shipping address to base address. + * + * @since 3.0.0 + * @throws WC_Data_Exception + */ + public function set_shipping_address_to_base() { + $base = wc_get_customer_default_location(); + $this->set_shipping_location( $base['country'], $base['state'], '', '' ); + } + + /** + * Sets all address info at once. + * + * @param string $country + * @param string $state + * @param string $postcode + * @param string $city + * @throws WC_Data_Exception + */ + public function set_billing_location( $country, $state = '', $postcode = '', $city = '' ) { + $billing = $this->get_prop( 'billing', 'edit' ); + $billing['country'] = $country; + $billing['state'] = $state; + $billing['postcode'] = $postcode; + $billing['city'] = $city; + $this->set_prop( 'billing', $billing ); + } + + /** + * Sets all shipping info at once. + * + * @param string $country + * @param string $state + * @param string $postcode + * @param string $city + * @throws WC_Data_Exception + */ + public function set_shipping_location( $country, $state = '', $postcode = '', $city = '' ) { + $shipping = $this->get_prop( 'shipping', 'edit' ); + $shipping['country'] = $country; + $shipping['state'] = $state; + $shipping['postcode'] = $postcode; + $shipping['city'] = $city; + $this->set_prop( 'shipping', $shipping ); + } + + /** + * Sets a prop for a setter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to set. + * @param string $address Name of address to set. billing or shipping. + * @param mixed $value Value of the prop. + */ + protected function set_address_prop( $prop, $address = 'billing', $value ) { + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + if ( true === $this->object_read ) { + if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { + $this->changes[ $address ][ $prop ] = $value; + } + } else { + $this->data[ $address ][ $prop ] = $value; + } + } + } + + /** + * Set billing_first_name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_first_name( $value ) { + $this->set_address_prop( 'first_name', 'billing', $value ); + } + + /** + * Set billing_last_name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_last_name( $value ) { + $this->set_address_prop( 'last_name', 'billing', $value ); + } + + /** + * Set billing_company. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_company( $value ) { + $this->set_address_prop( 'company', 'billing', $value ); + } + + /** + * Set billing_address_1. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_address( $value ) { + $this->set_billing_address_1( $value ); + } + + /** + * Set billing_address_1. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_address_1( $value ) { + $this->set_address_prop( 'address_1', 'billing', $value ); + } + + /** + * Set billing_address_2. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_address_2( $value ) { + $this->set_address_prop( 'address_2', 'billing', $value ); + } + + /** + * Set billing_city. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_city( $value ) { + $this->set_address_prop( 'city', 'billing', $value ); + } + + /** + * Set billing_state. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_state( $value ) { + $this->set_address_prop( 'state', 'billing', $value ); + } + + /** + * Set billing_postcode. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_postcode( $value ) { + $this->set_address_prop( 'postcode', 'billing', $value ); + } + + /** + * Set billing_country. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_country( $value ) { + $this->set_address_prop( 'country', 'billing', $value ); + } + + /** + * Set billing_email. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_email( $value ) { + if ( $value && ! is_email( $value ) ) { + $this->error( 'customer_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); + } + $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); + } + + /** + * Set billing_phone. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_phone( $value ) { + $this->set_address_prop( 'phone', 'billing', $value ); + } + + /** + * Set shipping_first_name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_first_name( $value ) { + $this->set_address_prop( 'first_name', 'shipping', $value ); + } + + /** + * Set shipping_last_name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_last_name( $value ) { + $this->set_address_prop( 'last_name', 'shipping', $value ); + } + + /** + * Set shipping_company. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_company( $value ) { + $this->set_address_prop( 'company', 'shipping', $value ); + } + + /** + * Set shipping_address_1. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_address( $value ) { + $this->set_shipping_address_1( $value ); + } + + /** + * Set shipping_address_1. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_address_1( $value ) { + $this->set_address_prop( 'address_1', 'shipping', $value ); + } + + /** + * Set shipping_address_2. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_address_2( $value ) { + $this->set_address_prop( 'address_2', 'shipping', $value ); + } + + /** + * Set shipping_city. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_city( $value ) { + $this->set_address_prop( 'city', 'shipping', $value ); + } + + /** + * Set shipping_state. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_state( $value ) { + $this->set_address_prop( 'state', 'shipping', $value ); + } + + /** + * Set shipping_postcode. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_postcode( $value ) { + $this->set_address_prop( 'postcode', 'shipping', $value ); + } + + /** + * Set shipping_country. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_country( $value ) { + $this->set_address_prop( 'country', 'shipping', $value ); + } + + /** + * Set if the user a paying customer. + * + * @since 3.0.0 + * @param bool $is_paying_customer + * @throws WC_Data_Exception + */ + function set_is_paying_customer( $is_paying_customer ) { + $this->set_prop( 'is_paying_customer', (bool) $is_paying_customer ); + } } diff --git a/includes/class-wc-data-exception.php b/includes/class-wc-data-exception.php new file mode 100644 index 00000000000..504552ae36c --- /dev/null +++ b/includes/class-wc-data-exception.php @@ -0,0 +1,68 @@ +error_code = $code; + $this->error_data = array_merge( array( 'status' => $http_status_code ), $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; + } +} diff --git a/includes/class-wc-data-store.php b/includes/class-wc-data-store.php new file mode 100644 index 00000000000..5355368ab8e --- /dev/null +++ b/includes/class-wc-data-store.php @@ -0,0 +1,200 @@ + class name. + * Example: 'product' => 'WC_Product_Data_Store_CPT' + * You can also pass something like product_ for product stores and + * that type will be used first when avaiable, if a store is requested like + * this and doesn't exist, then the store would fall back to 'product'. + * Ran through `woocommerce_data_stores`. + */ + private $stores = array( + 'coupon' => 'WC_Coupon_Data_Store_CPT', + 'customer' => 'WC_Customer_Data_Store', + 'customer-download' => 'WC_Customer_Download_Data_Store', + 'customer-session' => 'WC_Customer_Data_Store_Session', + 'order' => 'WC_Order_Data_Store_CPT', + 'order-refund' => 'WC_Order_Refund_Data_Store_CPT', + 'order-item' => 'WC_Order_Item_Data_Store', + 'order-item-coupon' => 'WC_Order_Item_Coupon_Data_Store', + 'order-item-fee' => 'WC_Order_Item_Fee_Data_Store', + 'order-item-product' => 'WC_Order_Item_Product_Data_Store', + 'order-item-shipping' => 'WC_Order_Item_Shipping_Data_Store', + 'order-item-tax' => 'WC_Order_Item_Tax_Data_Store', + 'payment-token' => 'WC_Payment_Token_Data_Store', + 'product' => 'WC_Product_Data_Store_CPT', + 'product-grouped' => 'WC_Product_Grouped_Data_Store_CPT', + 'product-variable' => 'WC_Product_Variable_Data_Store_CPT', + 'product-variation' => 'WC_Product_Variation_Data_Store_CPT', + 'shipping-zone' => 'WC_Shipping_Zone_Data_Store', + ); + + /** + * Contains the name of the current data store's class name. + */ + private $current_class_name = ''; + + /** + * The object type this store works with. + * @var string + */ + private $object_type = ''; + + + /** + * Tells WC_Data_Store which object (coupon, product, order, etc) + * store we want to work with. + * + * @param string $object_type Name of object. + * + * @throws Exception + */ + public function __construct( $object_type ) { + $this->object_type = $object_type; + $this->stores = apply_filters( 'woocommerce_data_stores', $this->stores ); + + // If this object type can't be found, check to see if we can load one + // level up (so if product-type isn't found, we try product). + if ( ! array_key_exists( $object_type, $this->stores ) ) { + $pieces = explode( '-', $object_type ); + $object_type = $pieces[0]; + } + + if ( array_key_exists( $object_type, $this->stores ) ) { + $store = apply_filters( 'woocommerce_' . $object_type . '_data_store', $this->stores[ $object_type ] ); + if ( is_object( $store ) ) { + if ( ! $store instanceof WC_Object_Data_Store_Interface ) { + throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); + } + $this->current_class_name = get_class( $store ); + $this->instance = $store; + } else { + if ( ! class_exists( $store ) ) { + throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); + } + $this->current_class_name = $store; + $this->instance = new $store; + } + } else { + throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); + } + } + + /** + * Only store the object type to avoid serializing the data store instance. + * + * @return array + */ + public function __sleep() { + return array( 'object_type' ); + } + + /** + * Re-run the constructor with the object type. + */ + public function __wakeup() { + $this->__construct( $this->object_type ); + } + + /** + * Loads a data store. + * + * @param string $object_type Name of object. + * + * @since 3.0.0 + * @return WC_Data_Store + */ + public static function load( $object_type ) { + return new WC_Data_Store( $object_type ); + } + + /** + * Returns the class name of the current data store. + * + * @since 3.0.0 + * @return string + */ + public function get_current_class_name() { + return $this->current_class_name; + } + + /** + * Reads an object from the data store. + * + * @since 3.0.0 + * @param WC_Data + */ + public function read( &$data ) { + $this->instance->read( $data ); + } + + /** + * Create an object in the data store. + * + * @since 3.0.0 + * @param WC_Data + */ + public function create( &$data ) { + $this->instance->create( $data ); + } + + /** + * Update an object in the data store. + * + * @since 3.0.0 + * @param WC_Data + */ + public function update( &$data ) { + $this->instance->update( $data ); + } + + /** + * Delete an object from the data store. + * + * @since 3.0.0 + * @param WC_Data + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$data, $args = array() ) { + $this->instance->delete( $data, $args ); + } + + /** + * Data stores can define additional functions (for example, coupons have + * some helper methods for increasing or decreasing usage). This passes + * through to the instance if that function exists. + * + * @since 3.0.0 + * + * @param $method + * @param $parameters + * + * @return mixed + */ + public function __call( $method, $parameters ) { + if ( is_callable( array( $this->instance, $method ) ) ) { + $object = array_shift( $parameters ); + return call_user_func_array( array( $this->instance, $method ), array_merge( array( &$object ), $parameters ) ); + } + } + +} diff --git a/includes/class-wc-datetime.php b/includes/class-wc-datetime.php new file mode 100644 index 00000000000..fe7873d78f9 --- /dev/null +++ b/includes/class-wc-datetime.php @@ -0,0 +1,107 @@ +format( DATE_ATOM ); + } + + /** + * Set UTC offset. + * + * @param int $offset + */ + public function set_utc_offset( $offset ) { + $this->utc_offset = intval( $offset ); + } + + /** + * getOffset. + */ + public function getOffset() { + if ( $this->utc_offset ) { + return $this->utc_offset; + } else { + return parent::getOffset(); + } + } + + /** + * Set timezone. + * + * @param DateTimeZone $timezone + * + * @return DateTime + */ + public function setTimezone( $timezone ) { + $this->utc_offset = 0; + return parent::setTimezone( $timezone ); + } + + /** + * Missing in PHP 5.2. + * + * @since 3.0.0 + * @return int + */ + public function getTimestamp() { + return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); + } + + /** + * Get the timestamp with the WordPress timezone offset added or subtracted. + * + * @since 3.0.0 + * @return int + */ + public function getOffsetTimestamp() { + return $this->getTimestamp() + $this->getOffset(); + } + + /** + * Format a date based on the offset timestamp. + * + * @since 3.0.0 + * @param string $format + * @return string + */ + public function date( $format ) { + return gmdate( $format, $this->getOffsetTimestamp() ); + } + + /** + * Return a localised date based on offset timestamp. Wrapper for date_i18n function. + * + * @since 3.0.0 + * @param string $format + * @return string + */ + public function date_i18n( $format = 'Y-m-d' ) { + return date_i18n( $format, $this->getOffsetTimestamp() ); + } +} diff --git a/includes/class-wc-deprecated-action-hooks.php b/includes/class-wc-deprecated-action-hooks.php new file mode 100644 index 00000000000..6e1e10eaba9 --- /dev/null +++ b/includes/class-wc-deprecated-action-hooks.php @@ -0,0 +1,143 @@ + array( + 'woocommerce_order_add_shipping', + 'woocommerce_order_add_coupon', + 'woocommerce_order_add_tax', + 'woocommerce_order_add_fee', + 'woocommerce_add_shipping_order_item', + 'woocommerce_add_order_item_meta', + 'woocommerce_add_order_fee_meta', + ), + 'woocommerce_update_order_item' => array( + 'woocommerce_order_edit_product', + 'woocommerce_order_update_coupon', + 'woocommerce_order_update_shipping', + 'woocommerce_order_update_fee', + 'woocommerce_order_update_tax', + ), + 'woocommerce_new_payment_token' => 'woocommerce_payment_token_created', + 'woocommerce_new_product_variation' => 'woocommerce_create_product_variation', + ); + + /** + * Hook into the new hook so we can handle deprecated hooks once fired. + * @param string $hook_name + */ + public function hook_in( $hook_name ) { + add_action( $hook_name, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 ); + } + + /** + * If the old hook is in-use, trigger it. + * + * @param string $new_hook + * @param string $old_hook + * @param array $new_callback_args + * @param mixed $return_value + * @return mixed + */ + public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) { + if ( has_action( $old_hook ) ) { + $this->display_notice( $old_hook, $new_hook ); + $return_value = $this->trigger_hook( $old_hook, $new_callback_args ); + } + return $return_value; + } + + /** + * Fire off a legacy hook with it's args. + * + * @param string $old_hook + * @param array $new_callback_args + * @return mixed + */ + protected function trigger_hook( $old_hook, $new_callback_args ) { + switch ( $old_hook ) { + case 'woocommerce_order_add_shipping' : + case 'woocommerce_order_add_fee' : + $item_id = $new_callback_args[0]; + $item = $new_callback_args[1]; + $order_id = $new_callback_args[2]; + if ( is_a( $item, 'WC_Order_Item_Shipping' ) || is_a( $item, 'WC_Order_Item_Fee' ) ) { + do_action( $old_hook, $order_id, $item_id, $item ); + } + break; + case 'woocommerce_order_add_coupon' : + $item_id = $new_callback_args[0]; + $item = $new_callback_args[1]; + $order_id = $new_callback_args[2]; + if ( is_a( $item, 'WC_Order_Item_Coupon' ) ) { + do_action( $old_hook, $order_id, $item_id, $item->get_code(), $item->get_discount(), $item->get_discount_tax() ); + } + break; + case 'woocommerce_order_add_tax' : + $item_id = $new_callback_args[0]; + $item = $new_callback_args[1]; + $order_id = $new_callback_args[2]; + if ( is_a( $item, 'WC_Order_Item_Tax' ) ) { + do_action( $old_hook, $order_id, $item_id, $item->get_rate_id(), $item->get_tax_total(), $item->get_shipping_tax_total() ); + } + break; + case 'woocommerce_add_shipping_order_item' : + $item_id = $new_callback_args[0]; + $item = $new_callback_args[1]; + $order_id = $new_callback_args[2]; + if ( is_a( $item, 'WC_Order_Item_Shipping' ) ) { + do_action( $old_hook, $order_id, $item_id, $item->legacy_package_key ); + } + break; + case 'woocommerce_add_order_item_meta' : + $item_id = $new_callback_args[0]; + $item = $new_callback_args[1]; + $order_id = $new_callback_args[2]; + if ( is_a( $item, 'WC_Order_Item_Product' ) ) { + do_action( $old_hook, $item_id, $item->legacy_values, $item->legacy_cart_item_key ); + } + break; + case 'woocommerce_add_order_fee_meta' : + $item_id = $new_callback_args[0]; + $item = $new_callback_args[1]; + $order_id = $new_callback_args[2]; + if ( is_a( $item, 'WC_Order_Item_Fee' ) ) { + do_action( $old_hook, $order_id, $item_id, $item->legacy_fee, $item->legacy_fee_key ); + } + break; + case 'woocommerce_order_edit_product' : + $item_id = $new_callback_args[0]; + $item = $new_callback_args[1]; + $order_id = $new_callback_args[2]; + if ( is_a( $item, 'WC_Order_Item_Product' ) ) { + do_action( $old_hook, $order_id, $item_id, $item, $item->get_product() ); + } + break; + case 'woocommerce_order_update_coupon' : + case 'woocommerce_order_update_shipping' : + case 'woocommerce_order_update_fee' : + case 'woocommerce_order_update_tax' : + if ( ! is_a( $item, 'WC_Order_Item_Product' ) ) { + do_action( $old_hook, $order_id, $item_id, $item ); + } + break; + default : + do_action_ref_array( $old_hook, $new_callback_args ); + break; + } + } +} diff --git a/includes/class-wc-deprecated-filter-hooks.php b/includes/class-wc-deprecated-filter-hooks.php new file mode 100644 index 00000000000..3ad8d6fda5d --- /dev/null +++ b/includes/class-wc-deprecated-filter-hooks.php @@ -0,0 +1,92 @@ + 'old'. + * + * @var array + */ + protected $deprecated_hooks = array( + 'woocommerce_structured_data_order' => 'woocommerce_email_order_schema_markup', + 'woocommerce_add_to_cart_fragments' => 'add_to_cart_fragments', + 'woocommerce_add_to_cart_redirect' => 'add_to_cart_redirect', + 'woocommerce_product_get_width' => 'woocommerce_product_width', + 'woocommerce_product_get_height' => 'woocommerce_product_height', + 'woocommerce_product_get_length' => 'woocommerce_product_length', + 'woocommerce_product_get_weight' => 'woocommerce_product_weight', + 'woocommerce_product_get_sku' => 'woocommerce_get_sku', + 'woocommerce_product_get_price' => 'woocommerce_get_price', + 'woocommerce_product_get_regular_price' => 'woocommerce_get_regular_price', + 'woocommerce_product_get_sale_price' => 'woocommerce_get_sale_price', + 'woocommerce_product_get_tax_class' => 'woocommerce_product_tax_class', + 'woocommerce_product_get_stock_quantity' => 'woocommerce_get_stock_quantity', + 'woocommerce_product_get_attributes' => 'woocommerce_get_product_attributes', + 'woocommerce_product_get_gallery_image_ids' => 'woocommerce_product_gallery_attachment_ids', + 'woocommerce_product_get_review_count' => 'woocommerce_product_review_count', + 'woocommerce_product_get_downloads' => 'woocommerce_product_files', + 'woocommerce_order_get_currency' => 'woocommerce_get_currency', + 'woocommerce_order_get_discount_total' => 'woocommerce_order_amount_discount_total', + 'woocommerce_order_get_discount_tax' => 'woocommerce_order_amount_discount_tax', + 'woocommerce_order_get_shipping_total' => 'woocommerce_order_amount_shipping_total', + 'woocommerce_order_get_shipping_tax' => 'woocommerce_order_amount_shipping_tax', + 'woocommerce_order_get_cart_tax' => 'woocommerce_order_amount_cart_tax', + 'woocommerce_order_get_total' => 'woocommerce_order_amount_total', + 'woocommerce_order_get_total_tax' => 'woocommerce_order_amount_total_tax', + 'woocommerce_order_get_total_discount' => 'woocommerce_order_amount_total_discount', + 'woocommerce_order_get_subtotal' => 'woocommerce_order_amount_subtotal', + 'woocommerce_order_get_tax_totals' => 'woocommerce_order_tax_totals', + 'woocommerce_get_order_refund_get_amount' => 'woocommerce_refund_amount', + 'woocommerce_get_order_refund_get_reason' => 'woocommerce_refund_reason', + 'default_checkout_billing_country' => 'default_checkout_country', + 'default_checkout_billing_state' => 'default_checkout_state', + 'default_checkout_billing_postcode' => 'default_checkout_postcode', + 'woocommerce_system_status_environment_rows' => 'woocommerce_debug_posting', + 'woocommerce_credit_card_type_labels' => 'wocommerce_credit_card_type_labels', + ); + + /** + * Hook into the new hook so we can handle deprecated hooks once fired. + * @param string $hook_name + */ + public function hook_in( $hook_name ) { + add_filter( $hook_name, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 ); + } + + /** + * If the old hook is in-use, trigger it. + * + * @param string $new_hook + * @param string $old_hook + * @param array $new_callback_args + * @param mixed $return_value + * @return mixed + */ + public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) { + if ( has_filter( $old_hook ) ) { + $this->display_notice( $old_hook, $new_hook ); + $return_value = $this->trigger_hook( $old_hook, $new_callback_args ); + } + return $return_value; + } + + /** + * Fire off a legacy hook with it's args. + * + * @param string $old_hook + * @param array $new_callback_args + * @return mixed + */ + protected function trigger_hook( $old_hook, $new_callback_args ) { + return apply_filters_ref_array( $old_hook, $new_callback_args ); + } +} diff --git a/includes/class-wc-download-handler.php b/includes/class-wc-download-handler.php index 368d64eaf0c..3f109521d7f 100644 --- a/includes/class-wc-download-handler.php +++ b/includes/class-wc-download-handler.php @@ -5,7 +5,7 @@ if ( ! defined( 'ABSPATH' ) ) { } /** - * Download handler + * Download handler. * * Handle digital downloads. * @@ -18,196 +18,266 @@ if ( ! defined( 'ABSPATH' ) ) { class WC_Download_Handler { /** - * Hook in methods + * Hook in methods. */ public static function init() { - add_action( 'init', array( __CLASS__, 'download_product' ) ); + if ( isset( $_GET['download_file'], $_GET['order'], $_GET['email'] ) ) { + add_action( 'init', array( __CLASS__, 'download_product' ) ); + } + add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 ); + add_action( 'woocommerce_download_file_xsendfile', array( __CLASS__, 'download_file_xsendfile' ), 10, 2 ); + add_action( 'woocommerce_download_file_force', array( __CLASS__, 'download_file_force' ), 10, 2 ); } /** - * Check if we need to download a file and check validity + * Check if we need to download a file and check validity. */ public static function download_product() { - if ( isset( $_GET['download_file'] ) && isset( $_GET['order'] ) && isset( $_GET['email'] ) ) { + $product_id = absint( $_GET['download_file'] ); + $product = wc_get_product( $product_id ); + $data_store = WC_Data_Store::load( 'customer-download' ); - global $wpdb; + if ( ! $product || ! isset( $_GET['key'], $_GET['order'] ) ) { + self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); + } - $product_id = (int) $_GET['download_file']; - $order_key = $_GET['order']; - $email = sanitize_email( str_replace( ' ', '+', $_GET['email'] ) ); - $download_id = isset( $_GET['key'] ) ? preg_replace( '/\s+/', ' ', $_GET['key'] ) : ''; - $_product = get_product( $product_id ); + $download_ids = $data_store->get_downloads( array( + 'user_email' => sanitize_email( str_replace( ' ', '+', $_GET['email'] ) ), + 'order_key' => wc_clean( $_GET['order'] ), + 'product_id' => $product_id, + 'download_id' => wc_clean( preg_replace( '/\s+/', ' ', $_GET['key'] ) ), + 'orderby' => 'downloads_remaining', + 'order' => 'DESC', + 'limit' => 1, + 'return' => 'ids', + ) ); - if ( ! is_email( $email) ) { - wp_die( __( 'Invalid email address.', 'woocommerce' ) . ' ' . __( 'Go to homepage', 'woocommerce' ) . '', '', array( 'response' => 403 ) ); - } + if ( empty( $download_ids ) ) { + self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); + } - $query = " - SELECT order_id,downloads_remaining,user_id,download_count,access_expires,download_id - FROM " . $wpdb->prefix . "woocommerce_downloadable_product_permissions - WHERE user_email = %s - AND order_key = %s - AND product_id = %s"; + $download = new WC_Customer_Download( current( $download_ids ) ); - $args = array( - $email, - $order_key, - $product_id - ); + self::check_order_is_valid( $download ); + self::check_downloads_remaining( $download ); + self::check_download_expiry( $download ); + self::check_download_login_required( $download ); - if ( $download_id ) { - // backwards compatibility for existing download URLs - $query .= " AND download_id = %s"; - $args[] = $download_id; - } + do_action( + 'woocommerce_download_product', + $download->get_user_email(), + $download->get_order_key(), + $download->get_product_id(), + $download->get_user_id(), + $download->get_download_id(), + $download->get_order_id() + ); + $count = $download->get_download_count(); + $remaining = $download->get_downloads_remaining(); + $download->set_download_count( $count + 1 ); + if ( '' !== $remaining ) { + $download->set_downloads_remaining( $remaining - 1 ); + } + $download->save(); - $download_result = $wpdb->get_row( $wpdb->prepare( $query, $args ) ); + self::download( $product->get_file_download_path( $download->get_download_id() ), $download->get_product_id() ); + } - if ( ! $download_result ) { - wp_die( __( 'Invalid download.', 'woocommerce' ) . ' ' . __( 'Go to homepage', 'woocommerce' ) . '', '', array( 'response' => 404 ) ); - } - - $download_id = $download_result->download_id; - $order_id = $download_result->order_id; - $downloads_remaining = $download_result->downloads_remaining; - $download_count = $download_result->download_count; - $user_id = $download_result->user_id; - $access_expires = $download_result->access_expires; - - if ( $user_id && get_option( 'woocommerce_downloads_require_login' ) == 'yes' ) { - - if ( ! is_user_logged_in() ) { - wp_die( __( 'You must be logged in to download files.', 'woocommerce' ) . ' ' . __( 'Login', 'woocommerce' ) . '', __( 'Log in to Download Files', 'woocommerce' ), '', array( 'response' => 403 ) ); - } elseif ( ! current_user_can( 'download_file', $download_result ) ) { - wp_die( __( 'This is not your download link.', 'woocommerce' ), '', array( 'response' => 403 ) ); - } - - } - - if ( ! get_post( $product_id ) ) { - wp_die( __( 'Product no longer exists.', 'woocommerce' ) . ' ' . __( 'Go to homepage', 'woocommerce' ) . '', '', array( 'response' => 404 ) ); - } - - if ( $order_id ) { - $order = get_order( $order_id ); - - if ( ! $order->is_download_permitted() || $order->post_status != 'wc-completed' ) { - wp_die( __( 'Invalid order.', 'woocommerce' ) . ' ' . __( 'Go to homepage', 'woocommerce' ) . '', '', array( 'response' => 404 ) ); - } - } - - if ( $downloads_remaining == '0' ) { - wp_die( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ) . ' ' . __( 'Go to homepage', 'woocommerce' ) . '', '', array( 'response' => 403 ) ); - } - - if ( $access_expires > 0 && strtotime( $access_expires) < current_time( 'timestamp' ) ) { - wp_die( __( 'Sorry, this download has expired', 'woocommerce' ) . ' ' . __( 'Go to homepage', 'woocommerce' ) . '', '', array( 'response' => 403 ) ); - } - - if ( $downloads_remaining > 0 ) { - $wpdb->update( $wpdb->prefix . "woocommerce_downloadable_product_permissions", array( - 'downloads_remaining' => $downloads_remaining - 1, - ), array( - 'user_email' => $email, - 'order_key' => $order_key, - 'product_id' => $product_id, - 'download_id' => $download_id - ), array( '%d' ), array( '%s', '%s', '%d', '%s' ) ); - } - - // Count the download - $wpdb->update( $wpdb->prefix . "woocommerce_downloadable_product_permissions", array( - 'download_count' => $download_count + 1, - ), array( - 'user_email' => $email, - 'order_key' => $order_key, - 'product_id' => $product_id, - 'download_id' => $download_id - ), array( '%d' ), array( '%s', '%s', '%d', '%s' ) ); - - // Trigger action - do_action( 'woocommerce_download_product', $email, $order_key, $product_id, $user_id, $download_id, $order_id ); - - // Get the download URL and try to replace the url with a path - $file_path = $_product->get_file_download_path( $download_id ); - - // Download it! - self::download( $file_path, $product_id ); + /** + * Check if an order is valid for downloading from. + * @param WC_Customer_Download $download + * @access private + */ + private static function check_order_is_valid( $download ) { + if ( $download->get_order_id() && ( $order = wc_get_order( $download->get_order_id() ) ) && ! $order->is_download_permitted() ) { + self::download_error( __( 'Invalid order.', 'woocommerce' ), '', 403 ); } } + /** + * Check if there are downloads remaining. + * @param WC_Customer_Download $download + * @access private + */ + private static function check_downloads_remaining( $download ) { + if ( '' !== $download->get_downloads_remaining() && 0 >= $download->get_downloads_remaining() ) { + self::download_error( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ), '', 403 ); + } + } + + /** + * Check if the download has expired. + * @param WC_Customer_Download $download + * @access private + */ + private static function check_download_expiry( $download ) { + if ( ! is_null( $download->get_access_expires() ) && $download->get_access_expires()->getTimestamp() < strtotime( 'midnight', current_time( 'timestamp', true ) ) ) { + self::download_error( __( 'Sorry, this download has expired', 'woocommerce' ), '', 403 ); + } + } + + /** + * Check if a download requires the user to login first. + * @param WC_Customer_Download $download + * @access private + */ + private static function check_download_login_required( $download ) { + if ( $download->get_user_id() && 'yes' === get_option( 'woocommerce_downloads_require_login' ) ) { + if ( ! is_user_logged_in() ) { + if ( wc_get_page_id( 'myaccount' ) ) { + wp_safe_redirect( add_query_arg( 'wc_error', urlencode( __( 'You must be logged in to download files.', 'woocommerce' ) ), wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } else { + self::download_error( __( 'You must be logged in to download files.', 'woocommerce' ) . ' ' . __( 'Login', 'woocommerce' ) . '', __( 'Log in to Download Files', 'woocommerce' ), 403 ); + } + } elseif ( ! current_user_can( 'download_file', $download ) ) { + self::download_error( __( 'This is not your download link.', 'woocommerce' ), '', 403 ); + } + } + } + + /** + * @deprecated + * + * @param $download_data + */ + public static function count_download( $download_data ) {} + /** * Download a file - hook into init function. + * @param string $file_path URL to file + * @param integer $product_id of the product being downloaded */ public static function download( $file_path, $product_id ) { - global $is_IE; - - $file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method' ), $product_id ); - if ( ! $file_path ) { - wp_die( __( 'No file defined', 'woocommerce' ) . ' ' . __( 'Go to homepage', 'woocommerce' ) . '', '', array( 'response' => 404 ) ); + self::download_error( __( 'No file defined', 'woocommerce' ) ); } - // Redirect to the file... - if ( $file_download_method == "redirect" ) { - header( 'Location: ' . $file_path ); + $filename = basename( $file_path ); + + if ( strstr( $filename, '?' ) ) { + $filename = current( explode( '?', $filename ) ); + } + + $filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); + $file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id ); + + // Add action to prevent issues in IE + add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) ); + + // Trigger download via one of the methods + do_action( 'woocommerce_download_file_' . $file_download_method, $file_path, $filename ); + } + + /** + * Redirect to a file to start the download. + * @param string $file_path + * @param string $filename + */ + public static function download_file_redirect( $file_path, $filename = '' ) { + header( 'Location: ' . $file_path ); + exit; + } + + /** + * Parse file path and see if its remote or local. + * @param string $file_path + * @return array + */ + public static function parse_file_path( $file_path ) { + $wp_uploads = wp_upload_dir(); + $wp_uploads_dir = $wp_uploads['basedir']; + $wp_uploads_url = $wp_uploads['baseurl']; + + // Replace uploads dir, site url etc with absolute counterparts if we can + $replacements = array( + $wp_uploads_url => $wp_uploads_dir, + network_site_url( '/', 'https' ) => ABSPATH, + network_site_url( '/', 'http' ) => ABSPATH, + site_url( '/', 'https' ) => ABSPATH, + site_url( '/', 'http' ) => ABSPATH, + ); + + $file_path = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path ); + $parsed_file_path = parse_url( $file_path ); + $remote_file = true; + + // See if path needs an abspath prepended to work + if ( file_exists( ABSPATH . $file_path ) ) { + $remote_file = false; + $file_path = ABSPATH . $file_path; + + } elseif ( '/wp-content' === substr( $file_path, 0, 11 ) ) { + $remote_file = false; + $file_path = realpath( WP_CONTENT_DIR . substr( $file_path, 11 ) ); + + // Check if we have an absolute path + } elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ) ) ) && isset( $parsed_file_path['path'] ) && file_exists( $parsed_file_path['path'] ) ) { + $remote_file = false; + $file_path = $parsed_file_path['path']; + } + + return array( + 'remote_file' => $remote_file, + 'file_path' => $file_path, + ); + } + + /** + * Download a file using X-Sendfile, X-Lighttpd-Sendfile, or X-Accel-Redirect if available. + * @param string $file_path + * @param string $filename + */ + public static function download_file_xsendfile( $file_path, $filename ) { + $parsed_file_path = self::parse_file_path( $file_path ); + + if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules() ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + header( "X-Sendfile: " . $parsed_file_path['file_path'] ); + exit; + } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + header( "X-Lighttpd-Sendfile: " . $parsed_file_path['file_path'] ); + exit; + } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + $xsendfile_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`', '', $parsed_file_path['file_path'] ), '/' ); + header( "X-Accel-Redirect: /$xsendfile_path" ); exit; } - // ...or serve it - $remote_file = true; - $parsed_file_path = parse_url( $file_path ); + // Fallback + self::download_file_force( $file_path, $filename ); + } - $wp_uploads = wp_upload_dir(); - $wp_uploads_dir = $wp_uploads['basedir']; - $wp_uploads_url = $wp_uploads['baseurl']; + /** + * Force download - this is the default method. + * @param string $file_path + * @param string $filename + */ + public static function download_file_force( $file_path, $filename ) { + $parsed_file_path = self::parse_file_path( $file_path ); - if ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ) ) ) && isset( $parsed_file_path['path'] ) && file_exists( $parsed_file_path['path'] ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); - /** This is an absolute path */ - $remote_file = false; - - } elseif( strpos( $file_path, $wp_uploads_url ) !== false ) { - - /** This is a local file given by URL so we need to figure out the path */ - $remote_file = false; - $file_path = str_replace( $wp_uploads_url, $wp_uploads_dir, $file_path ); - - } elseif( is_multisite() && ( strpos( $file_path, network_site_url( '/', 'http' ) ) !== false || strpos( $file_path, network_site_url( '/', 'https' ) ) !== false ) ) { - - /** This is a local file outside of wp-content so figure out the path */ - $remote_file = false; - // Try to replace network url - $file_path = str_replace( network_site_url( '/', 'https' ), ABSPATH, $file_path ); - $file_path = str_replace( network_site_url( '/', 'http' ), ABSPATH, $file_path ); - // Try to replace upload URL - $file_path = str_replace( $wp_uploads_url, $wp_uploads_dir, $file_path ); - - } elseif( strpos( $file_path, site_url( '/', 'http' ) ) !== false || strpos( $file_path, site_url( '/', 'https' ) ) !== false ) { - - /** This is a local file outside of wp-content so figure out the path */ - $remote_file = false; - $file_path = str_replace( site_url( '/', 'https' ), ABSPATH, $file_path ); - $file_path = str_replace( site_url( '/', 'http' ), ABSPATH, $file_path ); - - } elseif ( file_exists( ABSPATH . $file_path ) ) { - - /** Path needs an abspath to work */ - $remote_file = false; - $file_path = ABSPATH . $file_path; - } - - if ( ! $remote_file ) { - // Remove Query String - if ( strstr( $file_path, '?' ) ) { - $file_path = current( explode( '?', $file_path ) ); + if ( ! self::readfile_chunked( $parsed_file_path['file_path'] ) ) { + if ( $parsed_file_path['remote_file'] ) { + self::download_file_redirect( $file_path ); + } else { + self::download_error( __( 'File not found', 'woocommerce' ) ); } - - // Run realpath - $file_path = realpath( $file_path ); } - // Get extension and type + exit; + } + + /** + * Get content type of a download. + * @param string $file_path + * @return string + * @access private + */ + private static function get_download_content_type( $file_path ) { $file_extension = strtolower( substr( strrchr( $file_path, "." ), 1 ) ); $ctype = "application/force-download"; @@ -219,149 +289,120 @@ class WC_Download_Handler { } } - // Start setting headers - if ( ! ini_get('safe_mode') ) { - @set_time_limit(0); - } + return $ctype; + } - if ( function_exists( 'get_magic_quotes_runtime' ) && get_magic_quotes_runtime() ) { - @set_magic_quotes_runtime(0); - } - - if ( function_exists( 'apache_setenv' ) ) { - @apache_setenv( 'no-gzip', 1 ); - } - - @session_write_close(); - @ini_set( 'zlib.output_compression', 'Off' ); - - /** - * Prevents errors, for example: transfer closed with 3 bytes remaining to read - */ - if ( ob_get_length() ) { - - if ( ob_get_level() ) { - - $levels = ob_get_level(); - - for ( $i = 0; $i < $levels; $i++ ) { - ob_end_clean(); // Zip corruption fix - } - - } else { - ob_end_clean(); // Clear the output buffer - } - } - - if ( $is_IE && is_ssl() ) { - // IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set. - header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' ); - header( 'Cache-Control: private' ); - } else { - nocache_headers(); - } - - $filename = basename( $file_path ); - - if ( strstr( $filename, '?' ) ) { - $filename = current( explode( '?', $filename ) ); - } - - $filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); + /** + * Set headers for the download. + * @param string $file_path + * @param string $filename + * @access private + */ + private static function download_headers( $file_path, $filename ) { + self::check_server_config(); + self::clean_buffers(); + nocache_headers(); header( "X-Robots-Tag: noindex, nofollow", true ); - header( "Content-Type: " . $ctype ); + header( "Content-Type: " . self::get_download_content_type( $file_path ) ); header( "Content-Description: File Transfer" ); header( "Content-Disposition: attachment; filename=\"" . $filename . "\";" ); header( "Content-Transfer-Encoding: binary" ); - if ( $size = @filesize( $file_path ) ) { - header( "Content-Length: " . $size ); - } - - if ( $file_download_method == 'xsendfile' ) { - - // Path fix - kudos to Jason Judge - if ( getcwd() ) { - $file_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`' , '', $file_path ), '/' ); - } - - header( "Content-Disposition: attachment; filename=\"" . $filename . "\";" ); - - if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules() ) ) { - - header("X-Sendfile: $file_path"); - exit; - - } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) { - - header( "X-Lighttpd-Sendfile: $file_path" ); - exit; - - } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) { - - header( "X-Accel-Redirect: /$file_path" ); - exit; - - } - } - - if ( $remote_file ) { - self::readfile_chunked( $file_path ) || header( 'Location: ' . $file_path ); - } else { - self::readfile_chunked( $file_path ) || wp_die( __( 'File not found', 'woocommerce' ) . ' ' . __( 'Go to homepage', 'woocommerce' ) . '', '', array( 'response' => 404 ) ); - } - - exit; + if ( $size = @filesize( $file_path ) ) { + header( "Content-Length: " . $size ); + } } /** - * readfile_chunked - * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/ - * @param string $file - * @param bool $retbytes return bytes of file - * @return bool|int - * @todo Meaning of the return value? Last return is status of fclose? + * Check and set certain server config variables to ensure downloads work as intended. */ - public static function readfile_chunked( $file, $retbytes = true ) { - $chunksize = 1 * ( 1024 * 1024 ); - $buffer = ''; - $cnt = 0; + private static function check_server_config() { + wc_set_time_limit( 0 ); + if ( function_exists( 'get_magic_quotes_runtime' ) && get_magic_quotes_runtime() && version_compare( phpversion(), '5.4', '<' ) ) { + set_magic_quotes_runtime( 0 ); + } + if ( function_exists( 'apache_setenv' ) ) { + @apache_setenv( 'no-gzip', 1 ); + } + @ini_set( 'zlib.output_compression', 'Off' ); + @session_write_close(); + } - if ( file_exists( $file ) ) { - $handle = fopen( $file, 'r' ); - if ( $handle === FALSE ) { - return FALSE; - } - } elseif ( version_compare( PHP_VERSION, '5.4.0', '<' ) && ini_get( 'safe_mode' ) ) { - $handle = @fopen( $file, 'r' ); - if ( $handle === FALSE ) { - return FALSE; + /** + * Clean all output buffers. + * + * Can prevent errors, for example: transfer closed with 3 bytes remaining to read. + * + * @access private + */ + private static function clean_buffers() { + if ( ob_get_level() ) { + $levels = ob_get_level(); + for ( $i = 0; $i < $levels; $i++ ) { + @ob_end_clean(); } } else { - return FALSE; + @ob_end_clean(); + } + } + + /** + * readfile_chunked. + * + * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/. + * + * @param string $file + * @return bool Success or fail + */ + public static function readfile_chunked( $file ) { + $chunksize = 1024 * 1024; + $handle = @fopen( $file, 'r' ); + + if ( false === $handle ) { + return false; } - while ( ! feof( $handle ) ) { - $buffer = fread( $handle, $chunksize ); - echo $buffer; + while ( ! @feof( $handle ) ) { + echo @fread( $handle, $chunksize ); + if ( ob_get_length() ) { ob_flush(); flush(); } - - if ( $retbytes ) { - $cnt += strlen( $buffer ); - } } - $status = fclose( $handle ); + return @fclose( $handle ); + } - if ( $retbytes && $status ) { - return $cnt; + /** + * Filter headers for IE to fix issues over SSL. + * + * IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set. + * + * @param array $headers + * @return array + */ + public static function ie_nocache_headers_fix( $headers ) { + if ( is_ssl() && ! empty( $GLOBALS['is_IE'] ) ) { + $headers['Cache-Control'] = 'private'; + unset( $headers['Pragma'] ); } + return $headers; + } - return $status; + /** + * Die with an error message if the download fails. + * @param string $message + * @param string $title + * @param integer $status + * @access private + */ + private static function download_error( $message, $title = '', $status = 404 ) { + if ( ! strstr( $message, '' . esc_html__( 'Go to shop', 'woocommerce' ) . ''; + } + wp_die( $message, $title, array( 'response' => $status ) ); } } diff --git a/includes/class-wc-emails.php b/includes/class-wc-emails.php index 01d0ba5bfc5..13dfbe10f7d 100644 --- a/includes/class-wc-emails.php +++ b/includes/class-wc-emails.php @@ -1,49 +1,35 @@ push_to_queue( array( + 'filter' => current_filter(), + 'args' => func_get_args(), + ) ); + } else { + call_user_func_array( array( __CLASS__, 'send_transactional_email' ), func_get_args() ); + } + } + + /** + * Init the mailer instance and call the notifications for the current filter. + * + * @internal + * + * @param string $filter Filter name. + * @param array $args Email args (default: []). + */ + public static function send_queued_transactional_email( $filter = '', $args = array() ) { + if ( apply_filters( 'woocommerce_allow_send_queued_transactional_email', true, $filter, $args ) ) { + self::instance(); // Init self so emails exist. + + // Ensure gateways are loaded in case they need to insert data into the emails. + WC()->payment_gateways(); + WC()->shipping(); + + do_action_ref_array( $filter . '_notification', $args ); + } + } + + /** + * Init the mailer instance and call the notifications for the current filter. + * + * @internal + * + * @param array $args Email args (default: []). + */ + public static function send_transactional_email( $args = array() ) { + try { + $args = func_get_args(); + self::instance(); // Init self so emails exist. + do_action_ref_array( current_filter() . '_notification', $args ); + } catch ( Exception $e ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + trigger_error( 'Transactional email triggered fatal error for callback ' . current_filter(), E_USER_WARNING ); + } + } } /** * Constructor for the email class hooks in all emails that can be sent. * - * @access public - * @return void */ - function __construct() { - + public function __construct() { $this->init(); // Email Header, Footer and content hooks add_action( 'woocommerce_email_header', array( $this, 'email_header' ) ); add_action( 'woocommerce_email_footer', array( $this, 'email_footer' ) ); + add_action( 'woocommerce_email_order_details', array( $this, 'order_details' ), 10, 4 ); add_action( 'woocommerce_email_order_meta', array( $this, 'order_meta' ), 10, 3 ); + add_action( 'woocommerce_email_customer_details', array( $this, 'customer_details' ), 10, 3 ); + add_action( 'woocommerce_email_customer_details', array( $this, 'email_addresses' ), 20, 3 ); // Hooks for sending emails during store events add_action( 'woocommerce_low_stock_notification', array( $this, 'low_stock' ) ); @@ -101,99 +181,85 @@ class WC_Emails { } /** - * Init email classes + * Init email classes. */ - function init() { + public function init() { // Include email classes - include_once( 'abstracts/abstract-wc-email.php' ); + include_once( dirname( __FILE__ ) . '/emails/class-wc-email.php' ); - $this->emails['WC_Email_New_Order'] = include( 'emails/class-wc-email-new-order.php' ); - $this->emails['WC_Email_Customer_Processing_Order'] = include( 'emails/class-wc-email-customer-processing-order.php' ); - $this->emails['WC_Email_Customer_Completed_Order'] = include( 'emails/class-wc-email-customer-completed-order.php' ); - $this->emails['WC_Email_Customer_Invoice'] = include( 'emails/class-wc-email-customer-invoice.php' ); - $this->emails['WC_Email_Customer_Note'] = include( 'emails/class-wc-email-customer-note.php' ); - $this->emails['WC_Email_Customer_Reset_Password'] = include( 'emails/class-wc-email-customer-reset-password.php' ); - $this->emails['WC_Email_Customer_New_Account'] = include( 'emails/class-wc-email-customer-new-account.php' ); + $this->emails['WC_Email_New_Order'] = include( 'emails/class-wc-email-new-order.php' ); + $this->emails['WC_Email_Cancelled_Order'] = include( 'emails/class-wc-email-cancelled-order.php' ); + $this->emails['WC_Email_Failed_Order'] = include( 'emails/class-wc-email-failed-order.php' ); + $this->emails['WC_Email_Customer_On_Hold_Order'] = include( 'emails/class-wc-email-customer-on-hold-order.php' ); + $this->emails['WC_Email_Customer_Processing_Order'] = include( 'emails/class-wc-email-customer-processing-order.php' ); + $this->emails['WC_Email_Customer_Completed_Order'] = include( 'emails/class-wc-email-customer-completed-order.php' ); + $this->emails['WC_Email_Customer_Refunded_Order'] = include( 'emails/class-wc-email-customer-refunded-order.php' ); + $this->emails['WC_Email_Customer_Invoice'] = include( 'emails/class-wc-email-customer-invoice.php' ); + $this->emails['WC_Email_Customer_Note'] = include( 'emails/class-wc-email-customer-note.php' ); + $this->emails['WC_Email_Customer_Reset_Password'] = include( 'emails/class-wc-email-customer-reset-password.php' ); + $this->emails['WC_Email_Customer_New_Account'] = include( 'emails/class-wc-email-customer-new-account.php' ); $this->emails = apply_filters( 'woocommerce_email_classes', $this->emails ); + + // include css inliner + if ( ! class_exists( 'Emogrifier' ) && class_exists( 'DOMDocument' ) ) { + include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' ); + } } /** * Return the email classes - used in admin to load settings. * - * @access public * @return array */ - function get_emails() { + public function get_emails() { return $this->emails; } /** * Get from name for email. * - * @access public * @return string */ - function get_from_name() { - if ( ! $this->_from_name ) - $this->_from_name = get_option( 'woocommerce_email_from_name' ); - - return wp_specialchars_decode( $this->_from_name ); + public function get_from_name() { + return wp_specialchars_decode( get_option( 'woocommerce_email_from_name' ), ENT_QUOTES ); } /** * Get from email address. * - * @access public * @return string */ - function get_from_address() { - if ( ! $this->_from_address ) - $this->_from_address = get_option( 'woocommerce_email_from_address' ); - - return $this->_from_address; - } - - /** - * Get the content type for the email. - * - * @access public - * @return string - */ - function get_content_type() { - return $this->_content_type; + public function get_from_address() { + return sanitize_email( get_option( 'woocommerce_email_from_address' ) ); } /** * Get the email header. * - * @access public * @param mixed $email_heading heading for the email - * @return void */ - function email_header( $email_heading ) { + public function email_header( $email_heading ) { wc_get_template( 'emails/email-header.php', array( 'email_heading' => $email_heading ) ); } /** * Get the email footer. - * - * @access public - * @return void */ - function email_footer() { + public function email_footer() { wc_get_template( 'emails/email-footer.php' ); } /** * Wraps a message in the woocommerce mail template. * - * @access public * @param mixed $email_heading - * @param mixed $message + * @param string $message + * @param bool $plain_text + * * @return string */ - function wrap_message( $email_heading, $message, $plain_text = false ) { + public function wrap_message( $email_heading, $message, $plain_text = false ) { // Buffer ob_start(); @@ -212,57 +278,45 @@ class WC_Emails { /** * Send the email. * - * @access public * @param mixed $to * @param mixed $subject * @param mixed $message * @param string $headers (default: "Content-Type: text/html\r\n") * @param string $attachments (default: "") - * @param string $content_type (default: "text/html") - * @return void + * @return bool */ - function send( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = "", $content_type = 'text/html' ) { - - // Set content type - $this->_content_type = $content_type; - - // Filters for the email - add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); - add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); - add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); - + public function send( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = "" ) { // Send - wp_mail( $to, $subject, $message, $headers, $attachments ); - - // Unhook filters - remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); - remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); - remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); + $email = new WC_Email(); + return $email->send( $to, $subject, $message, $headers, $attachments ); } /** * Prepare and send the customer invoice email on demand. * - * @access public - * @param mixed $pay_for_order - * @return void + * @param int|WC_Order $order */ - function customer_invoice( $order ) { + public function customer_invoice( $order ) { $email = $this->emails['WC_Email_Customer_Invoice']; - $email->trigger( $order ); + + if ( ! is_object( $order ) ) { + $order = wc_get_order( absint( $order ) ); + } + + $email->trigger( $order->get_id(), $order ); } /** * Customer new account welcome email. * - * @access public * @param int $customer_id * @param array $new_customer_data - * @return void + * @param bool $password_generated */ - function customer_new_account( $customer_id, $new_customer_data = array(), $password_generated = false ) { - if ( ! $customer_id ) + public function customer_new_account( $customer_id, $new_customer_data = array(), $password_generated = false ) { + if ( ! $customer_id ) { return; + } $user_pass = ! empty( $new_customer_data['user_pass'] ) ? $new_customer_data['user_pass'] : ''; @@ -270,159 +324,247 @@ class WC_Emails { $email->trigger( $customer_id, $user_pass, $password_generated ); } + /** + * Show the order details table + * + * @param WC_Order $order + * @param bool $sent_to_admin + * @param bool $plain_text + * @param string $email + */ + public function order_details( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) { + if ( $plain_text ) { + wc_get_template( 'emails/plain/email-order-details.php', array( 'order' => $order, 'sent_to_admin' => $sent_to_admin, 'plain_text' => $plain_text, 'email' => $email ) ); + } else { + wc_get_template( 'emails/email-order-details.php', array( 'order' => $order, 'sent_to_admin' => $sent_to_admin, 'plain_text' => $plain_text, 'email' => $email ) ); + } + } + /** * Add order meta to email templates. * - * @access public * @param mixed $order * @param bool $sent_to_admin (default: false) * @param bool $plain_text (default: false) - * @return void + * @return string */ - function order_meta( $order, $sent_to_admin = false, $plain_text = false ) { + public function order_meta( $order, $sent_to_admin = false, $plain_text = false ) { + $fields = apply_filters( 'woocommerce_email_order_meta_fields', array(), $sent_to_admin, $order ); - $meta = array(); - $show_fields = apply_filters( 'woocommerce_email_order_meta_keys', array(), $sent_to_admin ); + /** + * Deprecated woocommerce_email_order_meta_keys filter. + * + * @since 2.3.0 + */ + $_fields = apply_filters( 'woocommerce_email_order_meta_keys', array(), $sent_to_admin ); - if ( $order->customer_note ) - $meta[ __( 'Note', 'woocommerce' ) ] = wptexturize( $order->customer_note ); - - if ( $show_fields ) - foreach ( $show_fields as $key => $field ) { - if ( is_numeric( $key ) ) + if ( $_fields ) { + foreach ( $_fields as $key => $field ) { + if ( is_numeric( $key ) ) { $key = $field; + } - $meta[ wptexturize( $key ) ] = wptexturize( get_post_meta( $order->id, $field, true ) ); + $fields[ $key ] = array( + 'label' => wptexturize( $key ), + 'value' => wptexturize( get_post_meta( $order->get_id(), $field, true ) ), + ); } + } - if ( sizeof( $meta ) > 0 ) { + if ( $fields ) { if ( $plain_text ) { - foreach ( $meta as $key => $value ) { - if ( $value ) { - echo $key . ': ' . $value . "\n"; + foreach ( $fields as $field ) { + if ( isset( $field['label'] ) && isset( $field['value'] ) && $field['value'] ) { + echo $field['label'] . ': ' . $field['value'] . "\n"; } } - } else { - foreach ( $meta as $key => $value ) { - if ( $value ) { - echo '

    ' . $key . ': ' . $value . '

    '; + foreach ( $fields as $field ) { + if ( isset( $field['label'] ) && isset( $field['value'] ) && $field['value'] ) { + echo '

    ' . $field['label'] . ': ' . $field['value'] . '

    '; } } } } } + /** + * Is customer detail field valid? + * @param array $field + * @return boolean + */ + public function customer_detail_field_is_valid( $field ) { + return isset( $field['label'] ) && ! empty( $field['value'] ); + } + + /** + * Add customer details to email templates. + * + * @param WC_Order $order + * @param bool $sent_to_admin (default: false) + * @param bool $plain_text (default: false) + * @return string + */ + public function customer_details( $order, $sent_to_admin = false, $plain_text = false ) { + if ( ! is_a( $order, 'WC_Order' ) ) { + return; + } + $fields = array(); + + if ( $order->get_customer_note() ) { + $fields['customer_note'] = array( + 'label' => __( 'Note', 'woocommerce' ), + 'value' => wptexturize( $order->get_customer_note() ), + ); + } + + if ( $order->get_billing_email() ) { + $fields['billing_email'] = array( + 'label' => __( 'Email address', 'woocommerce' ), + 'value' => wptexturize( $order->get_billing_email() ), + ); + } + + if ( $order->get_billing_phone() ) { + $fields['billing_phone'] = array( + 'label' => __( 'Phone', 'woocommerce' ), + 'value' => wptexturize( $order->get_billing_phone() ), + ); + } + + $fields = array_filter( apply_filters( 'woocommerce_email_customer_details_fields', $fields, $sent_to_admin, $order ), array( $this, 'customer_detail_field_is_valid' ) ); + + if ( $plain_text ) { + wc_get_template( 'emails/plain/email-customer-details.php', array( 'fields' => $fields ) ); + } else { + wc_get_template( 'emails/email-customer-details.php', array( 'fields' => $fields ) ); + } + } + + /** + * Get the email addresses. + * + * @param WC_Order $order + * @param bool $sent_to_admin + * @param bool $plain_text + */ + public function email_addresses( $order, $sent_to_admin = false, $plain_text = false ) { + if ( ! is_a( $order, 'WC_Order' ) ) { + return; + } + if ( $plain_text ) { + wc_get_template( 'emails/plain/email-addresses.php', array( 'order' => $order ) ); + } else { + wc_get_template( 'emails/email-addresses.php', array( 'order' => $order ) ); + } + } + + /** + * Get blog name formatted for emails. + * @return string + */ + private function get_blogname() { + return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + } + /** * Low stock notification email. * - * @access public - * @param mixed $product - * @return void + * @param WC_Product $product */ - function low_stock( $product ) { + public function low_stock( $product ) { + if ( 'no' === get_option( 'woocommerce_notify_low_stock', 'yes' ) ) { + return; + } - $blogname = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES); + $subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product low in stock', 'woocommerce' ) ); + /* translators: 1: product name 2: items in stock */ + $message = sprintf( + __( '%1$s is low in stock. There are %2$d left.', 'woocommerce' ), + html_entity_decode( strip_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ), + html_entity_decode( strip_tags( $product->get_stock_quantity() ) ) + ); - $subject = apply_filters( 'woocommerce_email_subject_low_stock', sprintf( '[%s] %s', $blogname, __( 'Product low in stock', 'woocommerce' ) ), $product ); - - $sku = ($product->sku) ? '(' . $product->sku . ') ' : ''; - - if ( ! empty( $product->variation_id ) ) - $title = sprintf(__( 'Variation #%s of %s', 'woocommerce' ), $product->variation_id, get_the_title($product->id)) . ' ' . $sku; - else - $title = sprintf(__( 'Product #%s - %s', 'woocommerce' ), $product->id, get_the_title($product->id)) . ' ' . $sku; - - $message = $title . __( 'is low in stock.', 'woocommerce' ); - - // CC, BCC, additional headers - $headers = apply_filters('woocommerce_email_headers', '', 'low_stock', $product); - - // Attachments - $attachments = apply_filters('woocommerce_email_attachments', array(), 'low_stock', $product); - - // Send the mail - wp_mail( get_option('woocommerce_stock_email_recipient'), $subject, $message, $headers, $attachments ); + wp_mail( + apply_filters( 'woocommerce_email_recipient_low_stock', get_option( 'woocommerce_stock_email_recipient' ), $product ), + apply_filters( 'woocommerce_email_subject_low_stock', $subject, $product ), + apply_filters( 'woocommerce_email_content_low_stock', $message, $product ), + apply_filters( 'woocommerce_email_headers', '', 'low_stock', $product ), + apply_filters( 'woocommerce_email_attachments', array(), 'low_stock', $product ) + ); } /** * No stock notification email. * - * @access public - * @param mixed $product - * @return void + * @param WC_Product $product */ - function no_stock( $product ) { + public function no_stock( $product ) { + if ( 'no' === get_option( 'woocommerce_notify_no_stock', 'yes' ) ) { + return; + } - $blogname = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES); + $subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product out of stock', 'woocommerce' ) ); + /* translators: %s: product name */ + $message = sprintf( __( '%s is out of stock.', 'woocommerce' ), html_entity_decode( strip_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ) ); - $subject = apply_filters( 'woocommerce_email_subject_no_stock', sprintf( '[%s] %s', $blogname, __( 'Product out of stock', 'woocommerce' ) ), $product ); - - $sku = ($product->sku) ? '(' . $product->sku . ') ' : ''; - - if ( ! empty( $product->variation_id ) ) - $title = sprintf(__( 'Variation #%s of %s', 'woocommerce' ), $product->variation_id, get_the_title($product->id)) . ' ' . $sku; - else - $title = sprintf(__( 'Product #%s - %s', 'woocommerce' ), $product->id, get_the_title($product->id)) . ' ' . $sku; - - $message = $title . __( 'is out of stock.', 'woocommerce' ); - - // CC, BCC, additional headers - $headers = apply_filters('woocommerce_email_headers', '', 'no_stock', $product); - - // Attachments - $attachments = apply_filters('woocommerce_email_attachments', array(), 'no_stock', $product); - - // Send the mail - wp_mail( get_option('woocommerce_stock_email_recipient'), $subject, $message, $headers, $attachments ); + wp_mail( + apply_filters( 'woocommerce_email_recipient_no_stock', get_option( 'woocommerce_stock_email_recipient' ), $product ), + apply_filters( 'woocommerce_email_subject_no_stock', $subject, $product ), + apply_filters( 'woocommerce_email_content_no_stock', $message, $product ), + apply_filters( 'woocommerce_email_headers', '', 'no_stock', $product ), + apply_filters( 'woocommerce_email_attachments', array(), 'no_stock', $product ) + ); } /** * Backorder notification email. * - * @access public - * @param mixed $args - * @return void + * @param array $args */ - function backorder( $args ) { - - $defaults = array( - 'product' => '', + public function backorder( $args ) { + $args = wp_parse_args( $args, array( + 'product' => '', 'quantity' => '', - 'order_id' => '' - ); - - $args = wp_parse_args( $args, $defaults ); + 'order_id' => '', + ) ); extract( $args ); - if (!$product || !$quantity) return; + if ( ! $product || ! $quantity || ! ( $order = wc_get_order( $order_id ) ) ) { + return; + } - $blogname = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES); + $subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product backorder', 'woocommerce' ) ); + $message = sprintf( __( '%1$s units of %2$s have been backordered in order #%3$s.', 'woocommerce' ), $quantity, html_entity_decode( strip_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ), $order->get_order_number() ); - $subject = apply_filters( 'woocommerce_email_subject_backorder', sprintf( '[%s] %s', $blogname, __( 'Product Backorder', 'woocommerce' ) ), $product ); - - $sku = ($product->sku) ? ' (' . $product->sku . ')' : ''; - - if ( ! empty( $product->variation_id ) ) - $title = sprintf(__( 'Variation #%s of %s', 'woocommerce' ), $product->variation_id, get_the_title($product->id)) . $sku; - else - $title = sprintf(__( 'Product #%s - %s', 'woocommerce' ), $product->id, get_the_title($product->id)) . $sku; - - $order = get_order( $order_id ); - $message = sprintf(__( '%s units of %s have been backordered in order %s.', 'woocommerce' ), $quantity, $title, $order->get_order_number() ); - - // CC, BCC, additional headers - $headers = apply_filters('woocommerce_email_headers', '', 'backorder', $args); - - // Attachments - $attachments = apply_filters('woocommerce_email_attachments', array(), 'backorder', $args); - - // Send the mail - wp_mail( get_option('woocommerce_stock_email_recipient'), $subject, $message, $headers, $attachments ); + wp_mail( + apply_filters( 'woocommerce_email_recipient_backorder', get_option( 'woocommerce_stock_email_recipient' ), $args ), + apply_filters( 'woocommerce_email_subject_backorder', $subject, $args ), + apply_filters( 'woocommerce_email_content_backorder', $message, $args ), + apply_filters( 'woocommerce_email_headers', '', 'backorder', $args ), + apply_filters( 'woocommerce_email_attachments', array(), 'backorder', $args ) + ); } + /** + * Adds Schema.org markup for order in JSON-LD format. + * + * @deprecated 3.0.0 + * @see WC_Structured_Data::generate_order_data() + * + * @since 2.6.0 + * @param mixed $order + * @param bool $sent_to_admin (default: false) + * @param bool $plain_text (default: false) + */ + public function order_schema_markup( $order, $sent_to_admin = false, $plain_text = false ) { + wc_deprecated_function( 'WC_Emails::order_schema_markup', '3.0', 'WC_Structured_Data::generate_order_data' ); + + WC()->structured_data->generate_order_data( $order, $sent_to_admin, $plain_text ); + WC()->structured_data->output_structured_data(); + } } diff --git a/includes/class-wc-embed.php b/includes/class-wc-embed.php new file mode 100644 index 00000000000..d16d893668f --- /dev/null +++ b/includes/class-wc-embed.php @@ -0,0 +1,178 @@ +' . $_product->get_price_html() . '

    '; + + if ( ! empty( $post->post_excerpt ) ) { + ob_start(); + woocommerce_template_single_excerpt(); + $excerpt = ob_get_clean(); + } + + // Add the button. + $excerpt .= self::product_buttons(); + } + return $excerpt; + } + + /** + * Create the button to go to the product page for embedded products. + * + * @since 2.4.11 + * @return string + */ + public static function product_buttons() { + $_product = wc_get_product( get_the_ID() ); + $buttons = array(); + $button = '%s'; + + if ( $_product->is_type( 'simple' ) && $_product->is_purchasable() && $_product->is_in_stock() ) { + $buttons[] = sprintf( $button, esc_url( add_query_arg( 'add-to-cart', get_the_ID(), wc_get_cart_url() ) ), esc_html__( 'Buy now', 'woocommerce' ) ); + } + + $buttons[] = sprintf( $button, get_the_permalink(), esc_html__( 'Read more', 'woocommerce' ) ); + + return '

    ' . implode( ' ', $buttons ) . '

    '; + } + + /** + * Prints the markup for the rating stars. + * + * @since 2.4.11 + */ + public static function get_ratings() { + // Make sure we're only affecting embedded products. + if ( self::is_embedded_product() && ( $_product = wc_get_product( get_the_ID() ) ) && $_product->get_average_rating() > 0 ) { + ?> +
    + get_average_rating() ) + ); + ?> +
    + + + query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_key( $wp->query_vars['edit-address'] ), true ) : 'billing'; + $load_address = isset( $wp->query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_title( $wp->query_vars['edit-address'] ), true ) : 'billing'; $address = WC()->countries->get_address_fields( esc_attr( $_POST[ $load_address . '_country' ] ), $load_address . '_' ); @@ -66,27 +83,27 @@ class WC_Form_Handler { $field['type'] = 'text'; } - // Get Value + // Get Value. switch ( $field['type'] ) { - case "checkbox" : - $_POST[ $key ] = isset( $_POST[ $key ] ) ? 1 : 0; - break; + case 'checkbox' : + $_POST[ $key ] = (int) isset( $_POST[ $key ] ); + break; default : $_POST[ $key ] = isset( $_POST[ $key ] ) ? wc_clean( $_POST[ $key ] ) : ''; - break; + break; } - // Hook to allow modification of value + // Hook to allow modification of value. $_POST[ $key ] = apply_filters( 'woocommerce_process_myaccount_field_' . $key, $_POST[ $key ] ); - // Validation: Required fields + // Validation: Required fields. if ( ! empty( $field['required'] ) && empty( $_POST[ $key ] ) ) { - wc_add_notice( $field['label'] . ' ' . __( 'is a required field.', 'woocommerce' ), 'error' ); + wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), $field['label'] ), 'error' ); } if ( ! empty( $_POST[ $key ] ) ) { - // Validation rules + // Validation rules. if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) { foreach ( $field['validate'] as $rule ) { switch ( $rule ) { @@ -94,32 +111,34 @@ class WC_Form_Handler { $_POST[ $key ] = strtoupper( str_replace( ' ', '', $_POST[ $key ] ) ); if ( ! WC_Validation::is_postcode( $_POST[ $key ], $_POST[ $load_address . '_country' ] ) ) { - wc_add_notice( __( 'Please enter a valid postcode/ZIP.', 'woocommerce' ), 'error' ); + wc_add_notice( __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ), 'error' ); } else { $_POST[ $key ] = wc_format_postcode( $_POST[ $key ], $_POST[ $load_address . '_country' ] ); } - break; + break; case 'phone' : $_POST[ $key ] = wc_format_phone_number( $_POST[ $key ] ); if ( ! WC_Validation::is_phone( $_POST[ $key ] ) ) { - wc_add_notice( '' . $field['label'] . ' ' . __( 'is not a valid phone number.', 'woocommerce' ), 'error' ); + wc_add_notice( sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '' . $field['label'] . '' ), 'error' ); } - break; + break; case 'email' : $_POST[ $key ] = strtolower( $_POST[ $key ] ); if ( ! is_email( $_POST[ $key ] ) ) { - wc_add_notice( '' . $field['label'] . ' ' . __( 'is not a valid email address.', 'woocommerce' ), 'error' ); + wc_add_notice( sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '' . $field['label'] . '' ), 'error' ); } - break; + break; } } } } } - if ( wc_notice_count( 'error' ) == 0 ) { + do_action( 'woocommerce_after_save_address_validation', $user_id, $load_address, $address ); + + if ( 0 === wc_notice_count( 'error' ) ) { foreach ( $address as $key => $field ) { update_user_meta( $user_id, $key, $_POST[ $key ] ); @@ -129,7 +148,7 @@ class WC_Form_Handler { do_action( 'woocommerce_customer_save_address', $user_id, $load_address ); - wp_safe_redirect( get_permalink( wc_get_page_id('myaccount') ) ); + wp_safe_redirect( wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) ); exit; } } @@ -139,17 +158,14 @@ class WC_Form_Handler { */ public static function save_account_details() { - if ( 'POST' !== strtoupper( $_SERVER[ 'REQUEST_METHOD' ] ) ) { + if ( 'POST' !== strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { return; } - if ( empty( $_POST[ 'action' ] ) || ( 'save_account_details' !== $_POST[ 'action' ] ) || empty( $_POST['_wpnonce'] ) ) { + if ( empty( $_POST['action'] ) || 'save_account_details' !== $_POST['action'] || empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'save_account_details' ) ) { return; } - wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-save_account_details' ); - - $update = true; $errors = new WP_Error(); $user = new stdClass(); @@ -160,49 +176,57 @@ class WC_Form_Handler { return; } - $account_first_name = ! empty( $_POST[ 'account_first_name' ] ) ? wc_clean( $_POST[ 'account_first_name' ] ) : ''; - $account_last_name = ! empty( $_POST[ 'account_last_name' ] ) ? wc_clean( $_POST[ 'account_last_name' ] ) : ''; - $account_email = ! empty( $_POST[ 'account_email' ] ) ? sanitize_email( $_POST[ 'account_email' ] ) : ''; - $pass_cur = ! empty( $_POST[ 'password_current' ] ) ? $_POST[ 'password_current' ] : ''; - $pass1 = ! empty( $_POST[ 'password_1' ] ) ? $_POST[ 'password_1' ] : ''; - $pass2 = ! empty( $_POST[ 'password_2' ] ) ? $_POST[ 'password_2' ] : ''; + $account_first_name = ! empty( $_POST['account_first_name'] ) ? wc_clean( $_POST['account_first_name'] ) : ''; + $account_last_name = ! empty( $_POST['account_last_name'] ) ? wc_clean( $_POST['account_last_name'] ) : ''; + $account_email = ! empty( $_POST['account_email'] ) ? wc_clean( $_POST['account_email'] ) : ''; + $pass_cur = ! empty( $_POST['password_current'] ) ? $_POST['password_current'] : ''; + $pass1 = ! empty( $_POST['password_1'] ) ? $_POST['password_1'] : ''; + $pass2 = ! empty( $_POST['password_2'] ) ? $_POST['password_2'] : ''; $save_pass = true; $user->first_name = $account_first_name; $user->last_name = $account_last_name; - $user->user_email = $account_email; - $user->display_name = $user->first_name; - if ( empty( $account_first_name ) || empty( $account_last_name ) ) { - wc_add_notice( __( 'Please enter your name.', 'woocommerce' ), 'error' ); + // Prevent emails being displayed, or leave alone. + $user->display_name = is_email( $current_user->display_name ) ? $user->first_name : $current_user->display_name; + + // Handle required fields + $required_fields = apply_filters( 'woocommerce_save_account_details_required_fields', array( + 'account_first_name' => __( 'First name', 'woocommerce' ), + 'account_last_name' => __( 'Last name', 'woocommerce' ), + 'account_email' => __( 'Email address', 'woocommerce' ), + ) ); + + foreach ( $required_fields as $field_key => $field_name ) { + if ( empty( $_POST[ $field_key ] ) ) { + wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), '' . esc_html( $field_name ) . '' ), 'error' ); + } } - if ( empty( $account_email ) || ! is_email( $account_email ) ) { - wc_add_notice( __( 'Please provide a valid email address.', 'woocommerce' ), 'error' ); - } elseif ( email_exists( $account_email ) && $account_email !== $current_user->user_email ) { - wc_add_notice( __( 'This email address is already registered.', 'woocommerce' ), 'error' ); - } - - if ( ! empty( $pass1 ) && ! wp_check_password( $pass_cur, $current_user->user_pass, $current_user->ID ) ) { - wc_add_notice( __( 'Your current password is incorrect.', 'woocommerce' ), 'error' ); - $save_pass = false; + if ( $account_email ) { + $account_email = sanitize_email( $account_email ); + if ( ! is_email( $account_email ) ) { + wc_add_notice( __( 'Please provide a valid email address.', 'woocommerce' ), 'error' ); + } elseif ( email_exists( $account_email ) && $account_email !== $current_user->user_email ) { + wc_add_notice( __( 'This email address is already registered.', 'woocommerce' ), 'error' ); + } + $user->user_email = $account_email; } if ( ! empty( $pass_cur ) && empty( $pass1 ) && empty( $pass2 ) ) { wc_add_notice( __( 'Please fill out all password fields.', 'woocommerce' ), 'error' ); - $save_pass = false; } elseif ( ! empty( $pass1 ) && empty( $pass_cur ) ) { wc_add_notice( __( 'Please enter your current password.', 'woocommerce' ), 'error' ); - $save_pass = false; } elseif ( ! empty( $pass1 ) && empty( $pass2 ) ) { wc_add_notice( __( 'Please re-enter your password.', 'woocommerce' ), 'error' ); - $save_pass = false; - } elseif ( ! empty( $pass1 ) && $pass1 !== $pass2 ) { - wc_add_notice( __( 'Passwords do not match.', 'woocommerce' ), 'error' ); - + } elseif ( ( ! empty( $pass1 ) || ! empty( $pass2 ) ) && $pass1 !== $pass2 ) { + wc_add_notice( __( 'New passwords do not match.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ! empty( $pass1 ) && ! wp_check_password( $pass_cur, $current_user->user_pass, $current_user->ID ) ) { + wc_add_notice( __( 'Your current password is incorrect.', 'woocommerce' ), 'error' ); $save_pass = false; } @@ -211,7 +235,7 @@ class WC_Form_Handler { } // Allow plugins to return their own errors. - do_action_ref_array( 'user_profile_update_errors', array ( &$errors, $update, &$user ) ); + do_action_ref_array( 'woocommerce_save_account_details_errors', array( &$errors, &$user ) ); if ( $errors->get_error_messages() ) { foreach ( $errors->get_error_messages() as $error ) { @@ -221,13 +245,13 @@ class WC_Form_Handler { if ( wc_notice_count( 'error' ) === 0 ) { - wp_update_user( $user ) ; + wp_update_user( $user ); wc_add_notice( __( 'Account details changed successfully.', 'woocommerce' ) ); do_action( 'woocommerce_save_account_details', $user->ID ); - wp_safe_redirect( get_permalink( wc_get_page_id( 'myaccount' ) ) ); + wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit; } } @@ -238,8 +262,8 @@ class WC_Form_Handler { public static function checkout_action() { if ( isset( $_POST['woocommerce_checkout_place_order'] ) || isset( $_POST['woocommerce_checkout_update_totals'] ) ) { - if ( sizeof( WC()->cart->get_cart() ) == 0 ) { - wp_redirect( get_permalink( wc_get_page_id( 'cart' ) ) ); + if ( WC()->cart->is_empty() ) { + wp_redirect( wc_get_page_permalink( 'cart' ) ); exit; } @@ -264,40 +288,46 @@ class WC_Form_Handler { // Pay for existing order $order_key = $_GET['key']; $order_id = absint( $wp->query_vars['order-pay'] ); - $order = get_order( $order_id ); + $order = wc_get_order( $order_id ); - $valid_order_statuses = apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $order ); + if ( $order->get_id() == $order_id && $order->get_order_key() == $order_key && $order->needs_payment() ) { - if ( $order->id == $order_id && $order->order_key == $order_key && $order->has_status( $valid_order_statuses ) ) { + do_action( 'woocommerce_before_pay_action', $order ); - // Set customer location to order location - if ( $order->billing_country ) { - WC()->customer->set_country( $order->billing_country ); - } - if ( $order->billing_state ) { - WC()->customer->set_state( $order->billing_state ); - } - if ( $order->billing_postcode ) { - WC()->customer->set_postcode( $order->billing_postcode ); - } - if ( $order->billing_city ) { - WC()->customer->set_city( $order->billing_city ); + WC()->customer->set_props( array( + 'billing_country' => $order->get_billing_country() ? $order->get_billing_country() : null, + 'billing_state' => $order->get_billing_state() ? $order->get_billing_state() : null, + 'billing_postcode' => $order->get_billing_postcode() ? $order->get_billing_postcode() : null, + 'billing_city' => $order->get_billing_city() ? $order->get_billing_city() : null, + ) ); + WC()->customer->save(); + + // Terms + if ( ! empty( $_POST['terms-field'] ) && empty( $_POST['terms'] ) ) { + wc_add_notice( __( 'You must accept our Terms & Conditions.', 'woocommerce' ), 'error' ); + return; } // Update payment method if ( $order->needs_payment() ) { - $payment_method = wc_clean( $_POST['payment_method'] ); - + $payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : false; $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + if ( ! $payment_method ) { + wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); + return; + } + // Update meta update_post_meta( $order_id, '_payment_method', $payment_method ); if ( isset( $available_gateways[ $payment_method ] ) ) { $payment_method_title = $available_gateways[ $payment_method ]->get_title(); + } else { + $payment_method_title = ''; } - update_post_meta( $order_id, '_payment_method_title', $payment_method_title); + update_post_meta( $order_id, '_payment_method_title', $payment_method_title ); // Validate $available_gateways[ $payment_method ]->validate_fields(); @@ -308,13 +338,11 @@ class WC_Form_Handler { $result = $available_gateways[ $payment_method ]->process_payment( $order_id ); // Redirect to success/confirmation/payment page - if ( 'success' == $result['result'] ) { + if ( 'success' === $result['result'] ) { wp_redirect( $result['redirect'] ); exit; } - } - } else { // No payment was required for order $order->payment_complete(); @@ -322,8 +350,9 @@ class WC_Form_Handler { exit; } - } + do_action( 'woocommerce_after_pay_action', $order ); + } } } @@ -331,30 +360,74 @@ class WC_Form_Handler { * Process the add payment method form. */ public static function add_payment_method_action() { - if ( isset( $_POST['woocommerce_add_payment_method'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-add-payment-method' ) ) { + if ( isset( $_POST['woocommerce_add_payment_method'], $_POST['payment_method'], $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-add-payment-method' ) ) { ob_start(); $payment_method = wc_clean( $_POST['payment_method'] ); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - // Validate $available_gateways[ $payment_method ]->validate_fields(); // Process - if ( wc_error_count() == 0 ) { + if ( wc_notice_count( 'wc_errors' ) == 0 ) { $result = $available_gateways[ $payment_method ]->add_payment_method(); - // Redirect to success/confirmation/payment page - if ( $result['result'] == 'success' ) { - wc_add_message( __( 'Payment method added.', 'woocommerce' ) ); + if ( 'success' === $result['result'] ) { + wc_add_notice( __( 'Payment method added.', 'woocommerce' ) ); wp_redirect( $result['redirect'] ); exit(); } + } + } + } + + /** + * Process the delete payment method form. + */ + public static function delete_payment_method_action() { + global $wp; + + if ( isset( $wp->query_vars['delete-payment-method'] ) ) { + + $token_id = absint( $wp->query_vars['delete-payment-method'] ); + $token = WC_Payment_Tokens::get( $token_id ); + + if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'delete-payment-method-' . $token_id ) ) { + wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); + } else { + WC_Payment_Tokens::delete( $token_id ); + wc_add_notice( __( 'Payment method deleted.', 'woocommerce' ) ); } + wp_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); + exit(); + } + + } + + /** + * Process the delete payment method form. + */ + public static function set_default_payment_method_action() { + global $wp; + + if ( isset( $wp->query_vars['set-default-payment-method'] ) ) { + + $token_id = absint( $wp->query_vars['set-default-payment-method'] ); + $token = WC_Payment_Tokens::get( $token_id ); + + if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'set-default-payment-method-' . $token_id ) ) { + wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); + } else { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), intval( $token_id ) ); + wc_add_notice( __( 'This payment method was successfully set as your default.', 'woocommerce' ) ); + } + + wp_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); + exit(); } } @@ -364,26 +437,51 @@ class WC_Form_Handler { */ public static function update_cart_action() { - // Add Discount if ( ! empty( $_POST['apply_coupon'] ) && ! empty( $_POST['coupon_code'] ) ) { + + // Add Discount WC()->cart->add_discount( sanitize_text_field( $_POST['coupon_code'] ) ); - } - // Remove Coupon Codes - elseif ( isset( $_GET['remove_coupon'] ) ) { + } elseif ( isset( $_GET['remove_coupon'] ) ) { + // Remove Coupon Codes WC()->cart->remove_coupon( wc_clean( $_GET['remove_coupon'] ) ); - } + } elseif ( ! empty( $_GET['remove_item'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'woocommerce-cart' ) ) { - // Remove from cart - elseif ( ! empty( $_GET['remove_item'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'woocommerce-cart' ) ) { + // Remove from cart + $cart_item_key = sanitize_text_field( $_GET['remove_item'] ); - WC()->cart->set_quantity( $_GET['remove_item'], 0 ); + if ( $cart_item = WC()->cart->get_cart_item( $cart_item_key ) ) { + WC()->cart->remove_cart_item( $cart_item_key ); - wc_add_notice( __( 'Cart updated.', 'woocommerce' ) ); + $product = wc_get_product( $cart_item['product_id'] ); - $referer = wp_get_referer() ? wp_get_referer() : WC()->cart->get_cart_url(); + $item_removed_title = apply_filters( 'woocommerce_cart_item_removed_title', $product ? sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), $product->get_name() ) : __( 'Item', 'woocommerce' ), $cart_item ); + + // Don't show undo link if removed item is out of stock. + if ( $product->is_in_stock() && $product->has_enough_stock( $cart_item['quantity'] ) ) { + $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); + $removed_notice .= ' ' . __( 'Undo?', 'woocommerce' ) . ''; + } else { + $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); + } + + wc_add_notice( $removed_notice ); + } + + $referer = wp_get_referer() ? remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart' ), add_query_arg( 'removed_item', '1', wp_get_referer() ) ) : wc_get_cart_url(); + wp_safe_redirect( $referer ); + exit; + + } elseif ( ! empty( $_GET['undo_item'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'woocommerce-cart' ) ) { + + // Undo Cart Item + $cart_item_key = sanitize_text_field( $_GET['undo_item'] ); + + WC()->cart->restore_cart_item( $cart_item_key ); + + $referer = wp_get_referer() ? remove_query_arg( array( 'undo_item', '_wpnonce' ), wp_get_referer() ) : wc_get_cart_url(); wp_safe_redirect( $referer ); exit; @@ -395,36 +493,36 @@ class WC_Form_Handler { $cart_updated = false; $cart_totals = isset( $_POST['cart'] ) ? $_POST['cart'] : ''; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { + if ( ! WC()->cart->is_empty() && is_array( $cart_totals ) ) { foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { $_product = $values['data']; // Skip product if no updated quantity was posted - if ( ! isset( $cart_totals[ $cart_item_key ]['qty'] ) ) { + if ( ! isset( $cart_totals[ $cart_item_key ] ) || ! isset( $cart_totals[ $cart_item_key ]['qty'] ) ) { continue; } // Sanitize $quantity = apply_filters( 'woocommerce_stock_amount_cart_item', wc_stock_amount( preg_replace( "/[^0-9\.]/", '', $cart_totals[ $cart_item_key ]['qty'] ) ), $cart_item_key ); - if ( '' === $quantity || $quantity == $values['quantity'] ) + if ( '' === $quantity || $quantity == $values['quantity'] ) { continue; + } // Update cart validation $passed_validation = apply_filters( 'woocommerce_update_cart_validation', true, $cart_item_key, $values, $quantity ); // is_sold_individually if ( $_product->is_sold_individually() && $quantity > 1 ) { - wc_add_notice( sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $_product->get_title() ), 'error' ); + wc_add_notice( sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $_product->get_name() ), 'error' ); $passed_validation = false; } if ( $passed_validation ) { WC()->cart->set_quantity( $cart_item_key, $quantity, false ); + $cart_updated = true; } - - $cart_updated = true; } } @@ -437,11 +535,11 @@ class WC_Form_Handler { } if ( ! empty( $_POST['proceed'] ) ) { - wp_safe_redirect( WC()->cart->get_checkout_url() ); + wp_safe_redirect( wc_get_checkout_url() ); exit; } elseif ( $cart_updated ) { wc_add_notice( __( 'Cart updated.', 'woocommerce' ) ); - $referer = remove_query_arg( 'remove_coupon', ( wp_get_referer() ? wp_get_referer() : WC()->cart->get_cart_url() ) ); + $referer = remove_query_arg( array( 'remove_coupon', 'add-to-cart' ), ( wp_get_referer() ? wp_get_referer() : wc_get_cart_url() ) ); wp_safe_redirect( $referer ); exit; } @@ -462,36 +560,38 @@ class WC_Form_Handler { WC()->cart->empty_cart(); // Load the previous order - Stop if the order does not exist - $order = get_order( absint( $_GET['order_again'] ) ); + $order = wc_get_order( absint( $_GET['order_again'] ) ); - if ( empty( $order->id ) ) { + if ( ! $order->get_id() ) { return; } - if ( ! $order->has_status( 'completed' ) ) { + if ( ! $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_order_again', array( 'completed' ) ) ) ) { return; } // Make sure the user is allowed to order again. By default it check if the // previous order belonged to the current user. - if ( ! current_user_can( 'order_again', $order->id ) ) { + if ( ! current_user_can( 'order_again', $order->get_id() ) ) { return; } // Copy products from the order to the cart - foreach ( $order->get_items() as $item ) { + $order_items = $order->get_items(); + foreach ( $order_items as $item ) { // Load all product info including variation data - $product_id = (int) apply_filters( 'woocommerce_add_to_cart_product_id', $item['product_id'] ); - $quantity = (int) $item['qty']; - $variation_id = (int) $item['variation_id']; + $product_id = (int) apply_filters( 'woocommerce_add_to_cart_product_id', $item->get_product_id() ); + $quantity = $item->get_quantity(); + $variation_id = $item->get_variation_id(); $variations = array(); $cart_item_data = apply_filters( 'woocommerce_order_again_cart_item_data', array(), $item, $order ); - foreach ( $item['item_meta'] as $meta_name => $meta_value ) { - if ( taxonomy_is_product_attribute( $meta_name ) ) { - $variations[ $meta_name ] = $meta_value[0]; - } elseif ( meta_is_product_attribute( $meta_name, $meta_value, $product_id ) ) { - $variations[ $meta_name ] = $meta_value[0]; + foreach ( $item->get_meta_data() as $meta ) { + if ( taxonomy_is_product_attribute( $meta->key ) ) { + $term = get_term_by( 'slug', $meta->value, $meta->key ); + $variations[ $meta->key ] = $term ? $term->name : $meta->value; + } elseif ( meta_is_product_attribute( $meta->key, $meta->value, $product_id ) ) { + $variations[ $meta->key ] = $meta->value; } } @@ -503,11 +603,29 @@ class WC_Form_Handler { WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations, $cart_item_data ); } - do_action( 'woocommerce_ordered_again', $order->id ); + do_action( 'woocommerce_ordered_again', $order->get_id() ); + + $num_items_in_cart = count( WC()->cart->get_cart() ); + $num_items_in_original_order = count( $order_items ); + + if ( $num_items_in_original_order > $num_items_in_cart ) { + wc_add_notice( + sprintf( _n( + '%d item from your previous order is currently unavailable and could not be added to your cart.', + '%d items from your previous order are currently unavailable and could not be added to your cart.', + $num_items_in_original_order - $num_items_in_cart, + 'woocommerce' + ), $num_items_in_original_order - $num_items_in_cart ), + 'error' + ); + } + + if ( $num_items_in_cart > 0 ) { + wc_add_notice( __( 'The cart has been filled with the items from your previous order.', 'woocommerce' ) ); + } // Redirect to cart - wc_add_notice( __( 'The cart has been filled with the items from your previous order.', 'woocommerce' ) ); - wp_safe_redirect( WC()->cart->get_cart_url() ); + wp_safe_redirect( wc_get_cart_url() ); exit; } @@ -519,22 +637,23 @@ class WC_Form_Handler { $order_key = $_GET['order']; $order_id = absint( $_GET['order_id'] ); - $order = get_order( $order_id ); + $order = wc_get_order( $order_id ); $user_can_cancel = current_user_can( 'cancel_order', $order_id ); $order_can_cancel = $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ) ) ); $redirect = $_GET['redirect']; if ( $order->has_status( 'cancelled' ) ) { // Already cancelled - take no action - } elseif ( $user_can_cancel && $order_can_cancel && $order->id == $order_id && $order->order_key == $order_key && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'woocommerce-cancel_order' ) ) { + } elseif ( $user_can_cancel && $order_can_cancel && $order->get_id() === $order_id && $order->get_order_key() === $order_key ) { // Cancel the order + restore stock - $order->cancel_order( __('Order cancelled by customer.', 'woocommerce' ) ); + WC()->session->set( 'order_awaiting_payment', false ); + $order->update_status( 'cancelled', __( 'Order cancelled by customer.', 'woocommerce' ) ); // Message wc_add_notice( apply_filters( 'woocommerce_order_cancelled_notice', __( 'Your order was cancelled.', 'woocommerce' ) ), apply_filters( 'woocommerce_order_cancelled_notice_type', 'notice' ) ); - do_action( 'woocommerce_cancelled_order', $order->id ); + do_action( 'woocommerce_cancelled_order', $order->get_id() ); } elseif ( $user_can_cancel && ! $order_can_cancel ) { wc_add_notice( __( 'Your order can no longer be cancelled. Please contact us if you need assistance.', 'woocommerce' ), 'error' ); @@ -550,7 +669,7 @@ class WC_Form_Handler { } /** - * Add to cart action + * Add to cart action. * * Checks for a valid request, does validation (via hooks) and then redirects if valid. * @@ -563,28 +682,130 @@ class WC_Form_Handler { $product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $_REQUEST['add-to-cart'] ) ); $was_added_to_cart = false; - $added_to_cart = array(); - $adding_to_cart = get_product( $product_id ); - $add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->product_type, $adding_to_cart ); + $adding_to_cart = wc_get_product( $product_id ); + + if ( ! $adding_to_cart ) { + return; + } + + $add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart ); // Variable product handling if ( 'variable' === $add_to_cart_handler ) { + $was_added_to_cart = self::add_to_cart_handler_variable( $product_id ); - $variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( $_REQUEST['variation_id'] ); - $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( $_REQUEST['quantity'] ); - $all_variations_set = true; - $variations = array(); + // Grouped Products + } elseif ( 'grouped' === $add_to_cart_handler ) { + $was_added_to_cart = self::add_to_cart_handler_grouped( $product_id ); - // Only allow integer variation ID - if its not set, redirect to the product page - if ( empty( $variation_id ) ) { - wc_add_notice( __( 'Please choose product options…', 'woocommerce' ), 'error' ); - return; + // Custom Handler + } elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ) { + do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url ); + + // Simple Products + } else { + $was_added_to_cart = self::add_to_cart_handler_simple( $product_id ); + } + + // If we added the product to the cart we can now optionally do a redirect. + if ( $was_added_to_cart && wc_notice_count( 'error' ) === 0 ) { + // If has custom URL redirect there + if ( $url = apply_filters( 'woocommerce_add_to_cart_redirect', $url ) ) { + wp_safe_redirect( $url ); + exit; + } elseif ( get_option( 'woocommerce_cart_redirect_after_add' ) === 'yes' ) { + wp_safe_redirect( wc_get_cart_url() ); + exit; + } + } + } + + /** + * Handle adding simple products to the cart. + * @since 2.4.6 Split from add_to_cart_action + * @param int $product_id + * @return bool success or not + */ + private static function add_to_cart_handler_simple( $product_id ) { + $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( $_REQUEST['quantity'] ); + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); + + if ( $passed_validation && WC()->cart->add_to_cart( $product_id, $quantity ) !== false ) { + wc_add_to_cart_message( array( $product_id => $quantity ), true ); + return true; + } + return false; + } + + /** + * Handle adding grouped products to the cart. + * @since 2.4.6 Split from add_to_cart_action + * @param int $product_id + * @return bool success or not + */ + private static function add_to_cart_handler_grouped( $product_id ) { + $was_added_to_cart = false; + $added_to_cart = array(); + + if ( ! empty( $_REQUEST['quantity'] ) && is_array( $_REQUEST['quantity'] ) ) { + $quantity_set = false; + + foreach ( $_REQUEST['quantity'] as $item => $quantity ) { + if ( $quantity <= 0 ) { + continue; + } + $quantity_set = true; + + // Add to cart validation + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $item, $quantity ); + + if ( $passed_validation && WC()->cart->add_to_cart( $item, $quantity ) !== false ) { + $was_added_to_cart = true; + $added_to_cart[ $item ] = $quantity; + } } - $attributes = $adding_to_cart->get_attributes(); - $variation = get_product( $variation_id ); + if ( ! $was_added_to_cart && ! $quantity_set ) { + wc_add_notice( __( 'Please choose the quantity of items you wish to add to your cart…', 'woocommerce' ), 'error' ); + } elseif ( $was_added_to_cart ) { + wc_add_to_cart_message( $added_to_cart ); + return true; + } + } elseif ( $product_id ) { + /* Link on product archives */ + wc_add_notice( __( 'Please choose a product to add to your cart…', 'woocommerce' ), 'error' ); + } + return false; + } + + /** + * Handle adding variable products to the cart. + * @since 2.4.6 Split from add_to_cart_action + * @param int $product_id + * @return bool success or not + */ + private static function add_to_cart_handler_variable( $product_id ) { + $adding_to_cart = wc_get_product( $product_id ); + $variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( $_REQUEST['variation_id'] ); + $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( $_REQUEST['quantity'] ); + $missing_attributes = array(); + $variations = array(); + $attributes = $adding_to_cart->get_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' ); + $variation_id = $data_store->find_matching_product_variation( $adding_to_cart, wp_unslash( $_POST ) ); + } + + // Validate the attributes. + try { + if ( empty( $variation_id ) ) { + throw new Exception( __( 'Please choose product options…', 'woocommerce' ) ); + } + + $variation_data = wc_get_product_variation_attributes( $variation_id ); - // Verify all attributes foreach ( $attributes as $attribute ) { if ( ! $attribute['is_variation'] ) { continue; @@ -593,265 +814,185 @@ class WC_Form_Handler { $taxonomy = 'attribute_' . sanitize_title( $attribute['name'] ); if ( isset( $_REQUEST[ $taxonomy ] ) ) { - // Get value from post data - // Don't use wc_clean as it destroys sanitized characters - $value = sanitize_title( trim( stripslashes( $_REQUEST[ $taxonomy ] ) ) ); + if ( $attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $value = sanitize_title( stripslashes( $_REQUEST[ $taxonomy ] ) ); + } else { + $value = wc_clean( stripslashes( $_REQUEST[ $taxonomy ] ) ); + } // Get valid value from variation - $valid_value = $variation->variation_data[ $taxonomy ]; + $valid_value = isset( $variation_data[ $taxonomy ] ) ? $variation_data[ $taxonomy ] : ''; - // Allow if valid - if ( $valid_value == '' || $valid_value == $value ) { - if ( $attribute['is_taxonomy'] ) { - $variations[ $taxonomy ] = $value; - } - else { - // For custom attributes, get the name from the slug - $options = array_map( 'trim', explode( WC_DELIMITER, $attribute['value'] ) ); - foreach ( $options as $option ) { - if ( sanitize_title( $option ) == $value ) { - $value = $option; - break; - } - } - $variations[ $taxonomy ] = $value; - } - continue; + // Allow if valid or show error. + if ( $valid_value === $value ) { + $variations[ $taxonomy ] = $value; + // If valid values are empty, this is an 'any' variation so get all possible values. + } elseif ( '' === $valid_value && in_array( $value, $attribute->get_slugs() ) ) { + $variations[ $taxonomy ] = $value; + } else { + throw new Exception( sprintf( __( 'Invalid value posted for %s', 'woocommerce' ), wc_attribute_label( $attribute['name'] ) ) ); } - - } - - $all_variations_set = false; - } - - if ( $all_variations_set ) { - // Add to cart validation - $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations ); - - if ( $passed_validation ) { - if ( WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) ) { - wc_add_to_cart_message( $product_id ); - $was_added_to_cart = true; - $added_to_cart[] = $product_id; - } - } - } else { - wc_add_notice( __( 'Please choose product options…', 'woocommerce' ), 'error' ); - return; - } - - // Grouped Products - } elseif ( 'grouped' === $add_to_cart_handler ) { - - if ( ! empty( $_REQUEST['quantity'] ) && is_array( $_REQUEST['quantity'] ) ) { - - $quantity_set = false; - - foreach ( $_REQUEST['quantity'] as $item => $quantity ) { - if ( $quantity <= 0 ) { - continue; - } - - $quantity_set = true; - - // Add to cart validation - $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $item, $quantity ); - - if ( $passed_validation ) { - if ( WC()->cart->add_to_cart( $item, $quantity ) ) { - $was_added_to_cart = true; - $added_to_cart[] = $item; - } - } - } - - if ( $was_added_to_cart ) { - wc_add_to_cart_message( $added_to_cart ); - } - - if ( ! $was_added_to_cart && ! $quantity_set ) { - wc_add_notice( __( 'Please choose the quantity of items you wish to add to your cart…', 'woocommerce' ), 'error' ); - return; - } - - } elseif ( $product_id ) { - - /* Link on product archives */ - wc_add_notice( __( 'Please choose a product to add to your cart…', 'woocommerce' ), 'error' ); - return; - - } - - // Simple Products - } else { - - $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( $_REQUEST['quantity'] ); - - // Add to cart validation - $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); - - if ( $passed_validation ) { - // Add the product to the cart - if ( WC()->cart->add_to_cart( $product_id, $quantity ) ) { - wc_add_to_cart_message( $product_id ); - $was_added_to_cart = true; - $added_to_cart[] = $product_id; + } else { + $missing_attributes[] = wc_attribute_label( $attribute['name'] ); } } - + if ( ! empty( $missing_attributes ) ) { + throw new Exception( sprintf( _n( '%s is a required field', '%s are required fields', sizeof( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ) ); + } + } catch ( Exception $e ) { + wc_add_notice( $e->getMessage(), 'error' ); + return false; } - // If we added the product to the cart we can now optionally do a redirect. - if ( $was_added_to_cart && wc_notice_count( 'error' ) == 0 ) { - - $url = apply_filters( 'add_to_cart_redirect', $url ); - - // If has custom URL redirect there - if ( $url ) { - wp_safe_redirect( $url ); - exit; - } - - // Redirect to cart option - elseif ( get_option('woocommerce_cart_redirect_after_add') == 'yes' ) { - wp_safe_redirect( WC()->cart->get_cart_url() ); - exit; - } + // Add to cart validation + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations ); + if ( $passed_validation && WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) !== false ) { + wc_add_to_cart_message( array( $product_id => $quantity ), true ); + return true; } + return false; } /** * Process the login form. */ public static function process_login() { - if ( ! empty( $_POST['login'] ) && ! empty( $_POST['_wpnonce'] ) ) { + $nonce_value = isset( $_POST['_wpnonce'] ) ? $_POST['_wpnonce'] : ''; + $nonce_value = isset( $_POST['woocommerce-login-nonce'] ) ? $_POST['woocommerce-login-nonce'] : $nonce_value; - wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-login' ); + if ( ! empty( $_POST['login'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-login' ) ) { try { - $creds = array(); + $creds = array( + 'user_password' => $_POST['password'], + 'remember' => isset( $_POST['rememberme'] ), + ); + $username = trim( $_POST['username'] ); $validation_error = new WP_Error(); $validation_error = apply_filters( 'woocommerce_process_login_errors', $validation_error, $_POST['username'], $_POST['password'] ); if ( $validation_error->get_error_code() ) { - throw new Exception( '' . __( 'Error', 'woocommerce' ) . ': ' . $validation_error->get_error_message() ); + throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . $validation_error->get_error_message() ); } - if ( empty( $_POST['username'] ) ) { - throw new Exception( '' . __( 'Error', 'woocommerce' ) . ': ' . __( 'Username is required.', 'woocommerce' ) ); + if ( empty( $username ) ) { + throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . __( 'Username is required.', 'woocommerce' ) ); } - if ( empty( $_POST['password'] ) ) { - throw new Exception( '' . __( 'Error', 'woocommerce' ) . ': ' . __( 'Password is required.', 'woocommerce' ) ); - } + if ( is_email( $username ) && apply_filters( 'woocommerce_get_username_from_email', true ) ) { + $user = get_user_by( 'email', $username ); - if ( is_email( $_POST['username'] ) && apply_filters( 'woocommerce_get_username_from_email', true ) ) { - $user = get_user_by( 'email', $_POST['username'] ); - - if ( isset( $user->user_login ) ) { - $creds['user_login'] = $user->user_login; - } else { - throw new Exception( '' . __( 'Error', 'woocommerce' ) . ': ' . __( 'A user could not be found with this email address.', 'woocommerce' ) ); + if ( ! $user ) { + $user = get_user_by( 'login', $username ); } + if ( isset( $user->user_login ) ) { + $creds['user_login'] = $user->user_login; + } else { + throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . __( 'A user could not be found with this email address.', 'woocommerce' ) ); + } } else { - $creds['user_login'] = $_POST['username']; + $creds['user_login'] = $username; } - $creds['user_password'] = $_POST['password']; - $creds['remember'] = isset( $_POST['rememberme'] ); - $secure_cookie = is_ssl() ? true : false; - $user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), $secure_cookie ); + // On multisite, ensure user exists on current site, if not add them before allowing login. + if ( is_multisite() ) { + $user_data = get_user_by( 'login', $username ); + + if ( $user_data && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) { + add_user_to_blog( get_current_blog_id(), $user_data->ID, 'customer' ); + } + } + + // Perform the login + $user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), is_ssl() ); if ( is_wp_error( $user ) ) { - throw new Exception( $user->get_error_message() ); + $message = $user->get_error_message(); + $message = str_replace( '' . esc_html( $creds['user_login'] ) . '', '' . esc_html( $username ) . '', $message ); + throw new Exception( $message ); } else { if ( ! empty( $_POST['redirect'] ) ) { - $redirect = esc_url( $_POST['redirect'] ); + $redirect = $_POST['redirect']; } elseif ( wp_get_referer() ) { - $redirect = esc_url( wp_get_referer() ); + $redirect = wp_get_referer(); } else { - $redirect = esc_url( get_permalink( wc_get_page_id( 'myaccount' ) ) ); + $redirect = wc_get_page_permalink( 'myaccount' ); } - // Feedback - wc_add_notice( sprintf( __( 'You are now logged in as %s', 'woocommerce' ), $user->display_name ) ); - wp_redirect( apply_filters( 'woocommerce_login_redirect', $redirect, $user ) ); exit; } - - } catch (Exception $e) { - - wc_add_notice( apply_filters('login_errors', $e->getMessage() ), 'error' ); - + } catch ( Exception $e ) { + wc_add_notice( apply_filters( 'login_errors', $e->getMessage() ), 'error' ); + do_action( 'woocommerce_login_failed' ); } } } /** - * Handle reset password form + * Handle lost password form. + */ + public static function process_lost_password() { + if ( isset( $_POST['wc_reset_password'] ) && isset( $_POST['user_login'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'lost_password' ) ) { + $success = WC_Shortcode_My_Account::retrieve_password(); + + // If successful, redirect to my account with query arg set. + if ( $success ) { + wp_redirect( add_query_arg( 'reset-link-sent', 'true', wc_get_account_endpoint_url( 'lost-password' ) ) ); + exit; + } + } + } + + /** + * Handle reset password form. */ public static function process_reset_password() { - if ( ! isset( $_POST['wc_reset_password'] ) ) { + $posted_fields = array( 'wc_reset_password', 'password_1', 'password_2', 'reset_key', 'reset_login', '_wpnonce' ); + + foreach ( $posted_fields as $field ) { + if ( ! isset( $_POST[ $field ] ) ) { + return; + } + $posted_fields[ $field ] = $_POST[ $field ]; + } + + if ( ! wp_verify_nonce( $posted_fields['_wpnonce'], 'reset_password' ) ) { return; } - // process lost password form - if ( isset( $_POST['user_login'] ) && isset( $_POST['_wpnonce'] ) ) { - wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-lost_password' ); + $user = WC_Shortcode_My_Account::check_password_reset_key( $posted_fields['reset_key'], $posted_fields['reset_login'] ); - WC_Shortcode_My_Account::retrieve_password(); - } - - // process reset password form - if ( isset( $_POST['password_1'] ) && isset( $_POST['password_2'] ) && isset( $_POST['reset_key'] ) && isset( $_POST['reset_login'] ) && isset( $_POST['_wpnonce'] ) ) { - - // verify reset key again - $user = WC_Shortcode_My_Account::check_password_reset_key( $_POST['reset_key'], $_POST['reset_login'] ); - - if ( is_object( $user ) ) { - - // save these values into the form again in case of errors - $args['key'] = wc_clean( $_POST['reset_key'] ); - $args['login'] = wc_clean( $_POST['reset_login'] ); - - wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-reset_password' ); - - if ( empty( $_POST['password_1'] ) || empty( $_POST['password_2'] ) ) { - wc_add_notice( __( 'Please enter your password.', 'woocommerce' ), 'error' ); - $args['form'] = 'reset_password'; - } - - if ( $_POST[ 'password_1' ] !== $_POST[ 'password_2' ] ) { - wc_add_notice( __( 'Passwords do not match.', 'woocommerce' ), 'error' ); - $args['form'] = 'reset_password'; - } - - $errors = new WP_Error(); - do_action( 'validate_password_reset', $errors, $user ); - if ( $errors->get_error_messages() ) { - foreach ( $errors->get_error_messages() as $error ) { - wc_add_notice( $error, 'error'); - } - } - - if ( 0 == wc_notice_count( 'error' ) ) { - - WC_Shortcode_My_Account::reset_password( $user, $_POST['password_1'] ); - - do_action( 'woocommerce_customer_reset_password', $user ); - - wp_redirect( add_query_arg( 'reset', 'true', remove_query_arg( array( 'key', 'login' ) ) ) ); - exit; - } + if ( $user instanceof WP_User ) { + if ( empty( $posted_fields['password_1'] ) ) { + wc_add_notice( __( 'Please enter your password.', 'woocommerce' ), 'error' ); } + if ( $posted_fields['password_1'] !== $posted_fields['password_2'] ) { + wc_add_notice( __( 'Passwords do not match.', 'woocommerce' ), 'error' ); + } + + $errors = new WP_Error(); + + do_action( 'validate_password_reset', $errors, $user ); + + wc_add_wp_error_notices( $errors ); + + if ( 0 === wc_notice_count( 'error' ) ) { + WC_Shortcode_My_Account::reset_password( $user, $posted_fields['password_1'] ); + + do_action( 'woocommerce_customer_reset_password', $user ); + + wp_redirect( add_query_arg( 'password-reset', 'true', wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } } } @@ -859,66 +1000,43 @@ class WC_Form_Handler { * Process the registration form. */ public static function process_registration() { - if ( ! empty( $_POST['register'] ) ) { + $nonce_value = isset( $_POST['_wpnonce'] ) ? $_POST['_wpnonce'] : ''; + $nonce_value = isset( $_POST['woocommerce-register-nonce'] ) ? $_POST['woocommerce-register-nonce'] : $nonce_value; - wp_verify_nonce( $_POST['register'], 'woocommerce-register' ); - - if ( 'no' === get_option( 'woocommerce_registration_generate_username' ) ) { - $_username = $_POST['username']; - } else { - $_username = ''; - } - - if ( 'no' === get_option( 'woocommerce_registration_generate_password' ) ) { - $_password = $_POST['password']; - } else { - $_password = ''; - } + if ( ! empty( $_POST['register'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-register' ) ) { + $username = 'no' === get_option( 'woocommerce_registration_generate_username' ) ? $_POST['username'] : ''; + $password = 'no' === get_option( 'woocommerce_registration_generate_password' ) ? $_POST['password'] : ''; + $email = $_POST['email']; try { - $validation_error = new WP_Error(); - $validation_error = apply_filters( 'woocommerce_process_registration_errors', $validation_error, $_username, $_password, $_POST['email'] ); + $validation_error = apply_filters( 'woocommerce_process_registration_errors', $validation_error, $username, $password, $email ); if ( $validation_error->get_error_code() ) { - throw new Exception( '' . __( 'Error', 'woocommerce' ) . ': ' . $validation_error->get_error_message() ); + throw new Exception( $validation_error->get_error_message() ); } + // Anti-spam trap + if ( ! empty( $_POST['email_2'] ) ) { + throw new Exception( __( 'Anti-spam field was filled in.', 'woocommerce' ) ); + } + + $new_customer = wc_create_new_customer( sanitize_email( $email ), wc_clean( $username ), $password ); + + if ( is_wp_error( $new_customer ) ) { + throw new Exception( $new_customer->get_error_message() ); + } + + if ( apply_filters( 'woocommerce_registration_auth_new_customer', true, $new_customer ) ) { + wc_set_customer_auth_cookie( $new_customer ); + } + + wp_safe_redirect( apply_filters( 'woocommerce_registration_redirect', wp_get_referer() ? wp_get_referer() : wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } catch ( Exception $e ) { - - wc_add_notice( $e->getMessage(), 'error' ); - return; - + wc_add_notice( '' . __( 'Error:', 'woocommerce' ) . ' ' . $e->getMessage(), 'error' ); } - - $username = ! empty( $_username ) ? wc_clean( $_username ) : ''; - $email = ! empty( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : ''; - $password = $_password; - - // Anti-spam trap - if ( ! empty( $_POST['email_2'] ) ) { - wc_add_notice( '' . __( 'ERROR', 'woocommerce' ) . ': ' . __( 'Anti-spam field was filled in.', 'woocommerce' ), 'error' ); - return; - } - - $new_customer = wc_create_new_customer( $email, $username, $password ); - - if ( is_wp_error( $new_customer ) ) { - wc_add_notice( $new_customer->get_error_message(), 'error' ); - return; - } - - wc_set_customer_auth_cookie( $new_customer ); - - // Redirect - if ( wp_get_referer() ) { - $redirect = esc_url( wp_get_referer() ); - } else { - $redirect = esc_url( get_permalink( wc_get_page_id( 'myaccount' ) ) ); - } - - wp_redirect( apply_filters( 'woocommerce_registration_redirect', $redirect ) ); - exit; } } } diff --git a/includes/class-wc-frontend-scripts.php b/includes/class-wc-frontend-scripts.php index 3c8d03c3136..1f684eb8dbd 100644 --- a/includes/class-wc-frontend-scripts.php +++ b/includes/class-wc-frontend-scripts.php @@ -1,247 +1,587 @@ array( - 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/woocommerce-layout.css', + 'src' => self::get_asset_url( 'assets/css/woocommerce-layout.css' ), 'deps' => '', 'version' => WC_VERSION, - 'media' => 'all' + 'media' => 'all', + 'has_rtl' => true, ), 'woocommerce-smallscreen' => array( - 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/woocommerce-smallscreen.css', + 'src' => self::get_asset_url( 'assets/css/woocommerce-smallscreen.css' ), 'deps' => 'woocommerce-layout', 'version' => WC_VERSION, - 'media' => 'only screen and (max-width: ' . apply_filters( 'woocommerce_style_smallscreen_breakpoint', $breakpoint = '768px' ) . ')' + 'media' => 'only screen and (max-width: ' . apply_filters( 'woocommerce_style_smallscreen_breakpoint', $breakpoint = '768px' ) . ')', + 'has_rtl' => true, ), 'woocommerce-general' => array( - 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/woocommerce.css', + 'src' => self::get_asset_url( 'assets/css/woocommerce.css' ), 'deps' => '', 'version' => WC_VERSION, - 'media' => 'all' + 'media' => 'all', + 'has_rtl' => true, ), ) ); } /** - * Register/queue frontend scripts. + * Return protocol relative asset URL. * - * @access public - * @return void + * @param string $path + * + * @return string + */ + private static function get_asset_url( $path ) { + return apply_filters( 'woocommerce_get_asset_url', str_replace( array( 'http:', 'https:' ), '', plugins_url( $path, WC_PLUGIN_FILE ) ), $path ); + } + + /** + * Register a script for use. + * + * @uses wp_register_script() + * @access private + * @param string $handle + * @param string $path + * @param string[] $deps + * @param string $version + * @param boolean $in_footer + */ + private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { + self::$scripts[] = $handle; + wp_register_script( $handle, $path, $deps, $version, $in_footer ); + } + + /** + * Register and enqueue a script for use. + * + * @uses wp_enqueue_script() + * @access private + * @param string $handle + * @param string $path + * @param string[] $deps + * @param string $version + * @param boolean $in_footer + */ + private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { + if ( ! in_array( $handle, self::$scripts ) && $path ) { + self::register_script( $handle, $path, $deps, $version, $in_footer ); + } + wp_enqueue_script( $handle ); + } + + /** + * Register a style for use. + * + * @uses wp_register_style() + * @access private + * @param string $handle + * @param string $path + * @param string[] $deps + * @param string $version + * @param string $media + * @param boolean $has_rtl + */ + private static function register_style( $handle, $path, $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { + self::$styles[] = $handle; + wp_register_style( $handle, $path, $deps, $version, $media ); + + if ( $has_rtl ) { + wp_style_add_data( $handle, 'rtl', 'replace' ); + } + } + + /** + * Register and enqueue a styles for use. + * + * @uses wp_enqueue_style() + * @access private + * @param string $handle + * @param string $path + * @param string[] $deps + * @param string $version + * @param string $media + * @param boolean $has_rtl + */ + private static function enqueue_style( $handle, $path = '', $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { + if ( ! in_array( $handle, self::$styles ) && $path ) { + self::register_style( $handle, $path, $deps, $version, $media, $has_rtl ); + } + wp_enqueue_style( $handle ); + } + + /** + * Register all WC scripts. + */ + private static function register_scripts() { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + $register_scripts = array( + 'flexslider' => array( + 'src' => self::get_asset_url( 'assets/js/flexslider/jquery.flexslider' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '2.6.3', + ), + 'js-cookie' => array( + 'src' => self::get_asset_url( 'assets/js/js-cookie/js.cookie' . $suffix . '.js' ), + 'deps' => array(), + 'version' => '2.1.4', + ), + 'jquery-blockui' => array( + 'src' => self::get_asset_url( 'assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '2.70', + ), + 'jquery-cookie' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/jquery-cookie/jquery.cookie' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '1.4.1', + ), + 'jquery-payment' => array( + 'src' => self::get_asset_url( 'assets/js/jquery-payment/jquery.payment' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '3.0.0', + ), + 'photoswipe' => array( + 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe' . $suffix . '.js' ), + 'deps' => array(), + 'version' => '4.1.1', + ), + 'photoswipe-ui-default' => array( + 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe-ui-default' . $suffix . '.js' ), + 'deps' => array( 'photoswipe' ), + 'version' => '4.1.1', + ), + 'prettyPhoto' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '3.1.6', + ), + 'prettyPhoto-init' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto.init' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'prettyPhoto' ), + 'version' => WC_VERSION, + ), + 'select2' => array( + 'src' => self::get_asset_url( 'assets/js/select2/select2.full' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '4.0.3', + ), + 'wc-address-i18n' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/address-i18n' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => WC_VERSION, + ), + 'wc-add-payment-method' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-payment-method' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce' ), + 'version' => WC_VERSION, + ), + 'wc-cart' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/cart' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'wc-country-select', 'wc-address-i18n' ), + 'version' => WC_VERSION, + ), + 'wc-cart-fragments' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/cart-fragments' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'js-cookie' ), + 'version' => WC_VERSION, + ), + 'wc-checkout' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/checkout' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), + 'version' => WC_VERSION, + ), + 'wc-country-select' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/country-select' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => WC_VERSION, + ), + 'wc-credit-card-form' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/credit-card-form' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'jquery-payment' ), + 'version' => WC_VERSION, + ), + 'wc-add-to-cart' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => WC_VERSION, + ), + 'wc-add-to-cart-variation' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart-variation' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'wp-util' ), + 'version' => WC_VERSION, + ), + 'wc-geolocation' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/geolocation' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => WC_VERSION, + ), + 'wc-lost-password' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/lost-password' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce' ), + 'version' => WC_VERSION, + ), + 'wc-password-strength-meter' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/password-strength-meter' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'password-strength-meter' ), + 'version' => WC_VERSION, + ), + 'wc-single-product' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/single-product' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => WC_VERSION, + ), + 'woocommerce' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/woocommerce' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'jquery-blockui', 'js-cookie' ), + 'version' => WC_VERSION, + ), + 'zoom' => array( + 'src' => self::get_asset_url( 'assets/js/zoom/jquery.zoom' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '1.7.15', + ), + ); + foreach ( $register_scripts as $name => $props ) { + self::register_script( $name, $props['src'], $props['deps'], $props['version'] ); + } + } + + /** + * Register all WC sty;es. + */ + private static function register_styles() { + $register_styles = array( + 'photoswipe' => array( + 'src' => self::get_asset_url( 'assets/css/photoswipe/photoswipe.css' ), + 'deps' => array(), + 'version' => WC_VERSION, + 'has_rtl' => false, + ), + 'photoswipe-default-skin' => array( + 'src' => self::get_asset_url( 'assets/css/photoswipe/default-skin/default-skin.css' ), + 'deps' => array( 'photoswipe' ), + 'version' => WC_VERSION, + 'has_rtl' => false, + ), + 'select2' => array( + 'src' => self::get_asset_url( 'assets/css/select2.css' ), + 'deps' => array(), + 'version' => WC_VERSION, + 'has_rtl' => false, + ), + 'woocommerce_prettyPhoto_css' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/css/prettyPhoto.css' ), + 'deps' => array(), + 'version' => WC_VERSION, + 'has_rtl' => true, + ), + ); + foreach ( $register_styles as $name => $props ) { + self::register_style( $name, $props['src'], $props['deps'], $props['version'], 'all', $props['has_rtl'] ); + } + } + + /** + * Register/queue frontend scripts. */ public static function load_scripts() { global $post; - $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; - $lightbox_en = get_option( 'woocommerce_enable_lightbox' ) == 'yes' ? true : false; - $ajax_cart_en = get_option( 'woocommerce_enable_ajax_add_to_cart' ) == 'yes' ? true : false; - $assets_path = str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/'; - $frontend_script_path = $assets_path . 'js/frontend/'; + if ( ! did_action( 'before_woocommerce_init' ) ) { + return; + } - // Register any scripts for later use, or used as dependencies - wp_register_script( 'chosen', $assets_path . 'js/chosen/chosen.jquery' . $suffix . '.js', array( 'jquery' ), '1.0.0', true ); - wp_register_script( 'jquery-blockui', $assets_path . 'js/jquery-blockui/jquery.blockUI' . $suffix . '.js', array( 'jquery' ), '2.60', true ); - wp_register_script( 'jquery-payment', $assets_path . 'js/jquery-payment/jquery.payment' . $suffix . '.js', array( 'jquery' ), '1.0.2', true ); - wp_register_script( 'wc-credit-card-form', $assets_path . 'js/frontend/credit-card-form' . $suffix . '.js', array( 'jquery', 'jquery-payment' ), WC_VERSION, true ); + self::register_scripts(); + self::register_styles(); - wp_register_script( 'wc-add-to-cart-variation', $frontend_script_path . 'add-to-cart-variation' . $suffix . '.js', array( 'jquery' ), WC_VERSION, true ); - wp_register_script( 'wc-single-product', $frontend_script_path . 'single-product' . $suffix . '.js', array( 'jquery' ), WC_VERSION, true ); - wp_register_script( 'wc-country-select', $frontend_script_path . 'country-select' . $suffix . '.js', array( 'jquery' ), WC_VERSION, true ); - wp_register_script( 'wc-address-i18n', $frontend_script_path . 'address-i18n' . $suffix . '.js', array( 'jquery' ), WC_VERSION, true ); - wp_register_script( 'jquery-cookie', $assets_path . 'js/jquery-cookie/jquery.cookie' . $suffix . '.js', array( 'jquery' ), '1.3.1', true ); - - // Queue frontend scripts conditionally - if ( $ajax_cart_en ) - wp_enqueue_script( 'wc-add-to-cart', $frontend_script_path . 'add-to-cart' . $suffix . '.js', array( 'jquery' ), WC_VERSION, true ); - - if ( is_cart() ) - wp_enqueue_script( 'wc-cart', $frontend_script_path . 'cart' . $suffix . '.js', array( 'jquery', 'wc-country-select' ), WC_VERSION, true ); + if ( 'yes' === get_option( 'woocommerce_enable_ajax_add_to_cart' ) ) { + self::enqueue_script( 'wc-add-to-cart' ); + } + if ( is_cart() ) { + self::enqueue_script( 'wc-cart' ); + } + if ( is_checkout() || is_account_page() ) { + self::enqueue_script( 'select2' ); + self::enqueue_style( 'select2' ); + // Password strength meter. Load in checkout, account login and edit account page. + if ( ( 'no' === get_option( 'woocommerce_registration_generate_password' ) && ! is_user_logged_in() ) || is_edit_account_page() || is_lost_password_page() ) { + self::enqueue_script( 'wc-password-strength-meter' ); + } + } if ( is_checkout() ) { - - if ( get_option( 'woocommerce_enable_chosen' ) == 'yes' ) { - wp_enqueue_script( 'wc-chosen', $frontend_script_path . 'chosen-frontend' . $suffix . '.js', array( 'chosen' ), WC_VERSION, true ); - wp_enqueue_style( 'woocommerce_chosen_styles', $assets_path . 'css/chosen.css' ); - } - - wp_enqueue_script( 'wc-checkout', $frontend_script_path . 'checkout' . $suffix . '.js', array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), WC_VERSION, true ); + self::enqueue_script( 'wc-checkout' ); + } + if ( is_add_payment_method_page() ) { + self::enqueue_script( 'wc-add-payment-method' ); + } + if ( is_lost_password_page() ) { + self::enqueue_script( 'wc-lost-password' ); } - if ( is_page( get_option( 'woocommerce_myaccount_page_id' ) ) ) { - if ( get_option( 'woocommerce_enable_chosen' ) == 'yes' ) { - wp_enqueue_script( 'wc-chosen', $frontend_script_path . 'chosen-frontend' . $suffix . '.js', array( 'chosen' ), WC_VERSION, true ); - wp_enqueue_style( 'woocommerce_chosen_styles', $assets_path . 'css/chosen.css' ); + // Load gallery scripts on product pages only if supported. + if ( is_product() || ( ! empty( $post->post_content ) && strstr( $post->post_content, '[product_page' ) ) ) { + if ( current_theme_supports( 'wc-product-gallery-zoom' ) ) { + self::enqueue_script( 'zoom' ); + } + if ( current_theme_supports( 'wc-product-gallery-slider' ) ) { + self::enqueue_script( 'flexslider' ); + } + if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { + self::enqueue_script( 'photoswipe-ui-default' ); + self::enqueue_style( 'photoswipe-default-skin' ); + add_action( 'wp_footer', 'woocommerce_photoswipe' ); + } + self::enqueue_script( 'wc-single-product' ); + } + + if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) { + $ua = wc_get_user_agent(); // Exclude common bots from geolocation by user agent. + + if ( ! strstr( $ua, 'bot' ) && ! strstr( $ua, 'spider' ) && ! strstr( $ua, 'crawl' ) ) { + self::enqueue_script( 'wc-geolocation' ); } } - if ( is_add_payment_method_page() ) - wp_enqueue_script( 'wc-add-payment-method', $frontend_script_path . 'add-payment-method' . $suffix . '.js', array( 'jquery', 'woocommerce' ), WC_VERSION, true ); - - if ( $lightbox_en && ( is_product() || ( ! empty( $post->post_content ) && strstr( $post->post_content, '[product_page' ) ) ) ) { - wp_enqueue_script( 'prettyPhoto', $assets_path . 'js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js', array( 'jquery' ), '3.1.5', true ); - wp_enqueue_script( 'prettyPhoto-init', $assets_path . 'js/prettyPhoto/jquery.prettyPhoto.init' . $suffix . '.js', array( 'jquery','prettyPhoto' ), WC_VERSION, true ); - wp_enqueue_style( 'woocommerce_prettyPhoto_css', $assets_path . 'css/prettyPhoto.css' ); - } - - if ( is_product() ) - wp_enqueue_script( 'wc-single-product' ); - // Global frontend scripts - wp_enqueue_script( 'woocommerce', $frontend_script_path . 'woocommerce' . $suffix . '.js', array( 'jquery', 'jquery-blockui' ), WC_VERSION, true ); - wp_enqueue_script( 'wc-cart-fragments', $frontend_script_path . 'cart-fragments' . $suffix . '.js', array( 'jquery', 'jquery-cookie' ), WC_VERSION, true ); + self::enqueue_script( 'woocommerce' ); + self::enqueue_script( 'wc-cart-fragments' ); // CSS Styles - $enqueue_styles = self::get_styles(); - - if ( $enqueue_styles ) { + if ( $enqueue_styles = self::get_styles() ) { foreach ( $enqueue_styles as $handle => $args ) { - wp_enqueue_style( $handle, $args['src'], $args['deps'], $args['version'], $args['media'] ); + if ( ! isset( $args['has_rtl'] ) ) { + $args['has_rtl'] = false; + } + + self::enqueue_style( $handle, $args['src'], $args['deps'], $args['version'], $args['media'], $args['has_rtl'] ); } } } /** - * Localize scripts only when enqueued + * Localize a WC script once. + * @access private + * @since 2.3.0 this needs less wp_script_is() calls due to https://core.trac.wordpress.org/ticket/28404 being added in WP 4.0. + * @param string $handle */ - public static function localize_printed_scripts() { + private static function localize_script( $handle ) { + if ( ! in_array( $handle, self::$wp_localize_scripts ) && wp_script_is( $handle ) && ( $data = self::get_script_data( $handle ) ) ) { + $name = str_replace( '-', '_', $handle ) . '_params'; + self::$wp_localize_scripts[] = $handle; + wp_localize_script( $handle, $name, apply_filters( $name, $data ) ); + } + } + + /** + * Return data for script handles. + * @access private + * @param string $handle + * @return array|bool + */ + private static function get_script_data( $handle ) { global $wp; - $assets_path = str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/'; + switch ( $handle ) { + case 'woocommerce' : + return array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( "%%endpoint%%" ), + ); + break; + case 'wc-geolocation' : + return array( + 'wc_ajax_url' => WC_AJAX::get_endpoint( "%%endpoint%%" ), + 'home_url' => home_url(), + 'is_available' => ! ( is_cart() || is_account_page() || is_checkout() || is_customize_preview() ) ? '1' : '0', + 'hash' => isset( $_GET['v'] ) ? wc_clean( $_GET['v'] ) : '', + ); + break; + case 'wc-single-product' : + return array( + 'i18n_required_rating_text' => esc_attr__( 'Please select a rating', 'woocommerce' ), + 'review_rating_required' => get_option( 'woocommerce_review_rating_required' ), + 'flexslider' => apply_filters( 'woocommerce_single_product_carousel_options', array( + 'rtl' => is_rtl(), + 'animation' => 'slide', + 'smoothHeight' => true, + 'directionNav' => false, + 'controlNav' => 'thumbnails', + 'slideshow' => false, + 'animationSpeed' => 500, + 'animationLoop' => false, // Breaks photoswipe pagination if true. + ) ), + 'zoom_enabled' => apply_filters( 'woocommerce_single_product_zoom_enabled', get_theme_support( 'wc-product-gallery-zoom' ) ), + 'photoswipe_enabled' => apply_filters( 'woocommerce_single_product_photoswipe_enabled', get_theme_support( 'wc-product-gallery-lightbox' ) ), + 'photoswipe_options' => apply_filters( 'woocommerce_single_product_photoswipe_options', array( + 'shareEl' => false, + 'closeOnScroll' => false, + 'history' => false, + 'hideAnimationDuration' => 0, + 'showAnimationDuration' => 0, + ) ), + 'flexslider_enabled' => apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ), + ); + break; + case 'wc-checkout' : + return array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( "%%endpoint%%" ), + 'update_order_review_nonce' => wp_create_nonce( 'update-order-review' ), + 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), + 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), + 'option_guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), + 'checkout_url' => WC_AJAX::get_endpoint( "checkout" ), + 'is_checkout' => is_page( wc_get_page_id( 'checkout' ) ) && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) ? 1 : 0, + 'debug_mode' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'i18n_checkout_error' => esc_attr__( 'Error processing checkout. Please try again.', 'woocommerce' ), + ); + break; + case 'wc-address-i18n' : + return array( + 'locale' => json_encode( WC()->countries->get_country_locale() ), + 'locale_fields' => json_encode( WC()->countries->get_country_locale_field_selectors() ), + 'i18n_required_text' => esc_attr__( 'required', 'woocommerce' ), + ); + break; + case 'wc-cart' : + return array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( "%%endpoint%%" ), + 'update_shipping_method_nonce' => wp_create_nonce( "update-shipping-method" ), + 'apply_coupon_nonce' => wp_create_nonce( "apply-coupon" ), + 'remove_coupon_nonce' => wp_create_nonce( "remove-coupon" ), + ); + break; + case 'wc-cart-fragments' : + return array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( "%%endpoint%%" ), + 'fragment_name' => apply_filters( 'woocommerce_cart_fragment_name', 'wc_fragments_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) ) ), + ); + break; + case 'wc-add-to-cart' : + return array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( "%%endpoint%%" ), + 'i18n_view_cart' => esc_attr__( 'View cart', 'woocommerce' ), + 'cart_url' => apply_filters( 'woocommerce_add_to_cart_redirect', wc_get_cart_url() ), + 'is_cart' => is_cart(), + 'cart_redirect_after_add' => get_option( 'woocommerce_cart_redirect_after_add' ), + ); + break; + case 'wc-add-to-cart-variation' : + // We also need the wp.template for this script :) + wc_get_template( 'single-product/add-to-cart/variation.php' ); - if ( wp_script_is( 'woocommerce' ) ) { - wp_localize_script( 'woocommerce', 'woocommerce_params', apply_filters( 'woocommerce_params', array( - 'ajax_url' => WC()->ajax_url(), - 'ajax_loader_url' => apply_filters( 'woocommerce_ajax_loader_url', $assets_path . 'images/ajax-loader@2x.gif' ), - ) ) ); - } - if ( wp_script_is( 'wc-single-product' ) ) { - wp_localize_script( 'wc-single-product', 'wc_single_product_params', apply_filters( 'wc_single_product_params', array( - 'i18n_required_rating_text' => esc_attr__( 'Please select a rating', 'woocommerce' ), - 'review_rating_required' => get_option( 'woocommerce_review_rating_required' ), - ) ) ); - } - if ( wp_script_is( 'wc-checkout' ) ) { - wp_localize_script( 'wc-checkout', 'wc_checkout_params', apply_filters( 'wc_checkout_params', array( - 'ajax_url' => WC()->ajax_url(), - 'ajax_loader_url' => apply_filters( 'woocommerce_ajax_loader_url', $assets_path . 'images/ajax-loader@2x.gif' ), - 'update_order_review_nonce' => wp_create_nonce( "update-order-review" ), - 'apply_coupon_nonce' => wp_create_nonce( "apply-coupon" ), - 'option_guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), - 'checkout_url' => add_query_arg( 'action', 'woocommerce_checkout', WC()->ajax_url() ), - 'is_checkout' => is_page( wc_get_page_id( 'checkout' ) ) && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) ? 1 : 0 - ) ) ); - } - if ( wp_script_is( 'wc-address-i18n' ) || wp_script_is( 'wc-checkout' ) ) { - wp_localize_script( 'wc-address-i18n', 'wc_address_i18n_params', apply_filters( 'wc_address_i18n_params', array( - 'locale' => json_encode( WC()->countries->get_country_locale() ), - 'locale_fields' => json_encode( WC()->countries->get_country_locale_field_selectors() ), - 'i18n_required_text' => esc_attr__( 'required', 'woocommerce' ), - ) ) ); - } - if ( wp_script_is( 'wc-cart' ) ) { - wp_localize_script( 'wc-cart', 'wc_cart_params', apply_filters( 'wc_cart_params', array( - 'ajax_url' => WC()->ajax_url(), - 'ajax_loader_url' => apply_filters( 'woocommerce_ajax_loader_url', $assets_path . 'images/ajax-loader@2x.gif' ), - 'update_shipping_method_nonce' => wp_create_nonce( "update-shipping-method" ), - ) ) ); - } - if ( wp_script_is( 'wc-cart-fragments' ) ) { - wp_localize_script( 'wc-cart-fragments', 'wc_cart_fragments_params', apply_filters( 'wc_cart_fragments_params', array( - 'ajax_url' => WC()->ajax_url(), - 'fragment_name' => apply_filters( 'woocommerce_cart_fragment_name', 'wc_fragments' ) - ) ) ); - } - if ( wp_script_is( 'wc-add-to-cart' ) ) { - wp_localize_script( 'wc-add-to-cart', 'wc_add_to_cart_params', apply_filters( 'wc_add_to_cart_params', array( - 'ajax_url' => WC()->ajax_url(), - 'ajax_loader_url' => apply_filters( 'woocommerce_ajax_loader_url', $assets_path . 'images/ajax-loader@2x.gif' ), - 'i18n_view_cart' => esc_attr__( 'View Cart', 'woocommerce' ), - 'cart_url' => get_permalink( wc_get_page_id( 'cart' ) ), - 'is_cart' => is_cart(), - 'cart_redirect_after_add' => get_option( 'woocommerce_cart_redirect_after_add' ) - ) ) ); - } - if ( wp_script_is( 'wc-add-to-cart-variation' ) ) { - wp_localize_script( 'wc-add-to-cart-variation', 'wc_add_to_cart_variation_params', apply_filters( 'wc_add_to_cart_variation_params', array( - 'i18n_no_matching_variations_text' => esc_attr__( 'Sorry, no products matched your selection. Please choose a different combination.', 'woocommerce' ), - 'i18n_unavailable_text' => esc_attr__( 'Sorry, this product is unavailable. Please choose a different combination.', 'woocommerce' ), - ) ) ); - } - - if ( wp_script_is( 'wc-country-select' ) || wp_script_is( 'wc-cart' ) || wp_script_is( 'wc-checkout' ) ) { - wp_localize_script( 'wc-country-select', 'wc_country_select_params', apply_filters( 'wc_country_select_params', array( - 'countries' => json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), - 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), - ) ) ); + return array( + 'wc_ajax_url' => WC_AJAX::get_endpoint( "%%endpoint%%" ), + 'i18n_no_matching_variations_text' => esc_attr__( 'Sorry, no products matched your selection. Please choose a different combination.', 'woocommerce' ), + 'i18n_make_a_selection_text' => esc_attr__( 'Please select some product options before adding this product to your cart.', 'woocommerce' ), + 'i18n_unavailable_text' => esc_attr__( 'Sorry, this product is unavailable. Please choose a different combination.', 'woocommerce' ), + ); + break; + case 'wc-country-select' : + return array( + 'countries' => json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), + 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), + 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), + 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), + 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), + 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), + ); + break; + case 'wc-password-strength-meter' : + return array( + 'min_password_strength' => apply_filters( 'woocommerce_min_password_strength', 3 ), + 'i18n_password_error' => esc_attr__( 'Please enter a stronger password.', 'woocommerce' ), + 'i18n_password_hint' => esc_attr( wp_get_password_hint() ), + ); + break; } + return false; } /** - * WC requires jQuery 1.8 since it uses functions like .on() for events and .parseHTML. - * If, by the time wp_print_scrips is called, jQuery is outdated (i.e not - * using the version in core) we need to deregister it and register the - * core version of the file. - * - * @access public - * @return void + * Localize scripts only when enqueued. */ - public static function check_jquery() { - global $wp_scripts; - - // Enforce minimum version of jQuery - if ( ! empty( $wp_scripts->registered['jquery']->ver ) && ! empty( $wp_scripts->registered['jquery']->src ) && 0 >= version_compare( $wp_scripts->registered['jquery']->ver, '1.8' ) ) { - wp_deregister_script( 'jquery' ); - wp_register_script( 'jquery', '/wp-includes/js/jquery/jquery.js', array(), '1.8' ); - wp_enqueue_script( 'jquery' ); + public static function localize_printed_scripts() { + foreach ( self::$scripts as $handle ) { + self::localize_script( $handle ); } } - - /** - * Provide backwards compat for old constant - * @param array $styles - * @return array - */ - public static function backwards_compat( $styles ) { - if ( defined( 'WOOCOMMERCE_USE_CSS' ) ) { - - _deprecated_function( 'WOOCOMMERCE_USE_CSS', '2.1', 'Styles should be removed using wp_deregister_style or the woocommerce_enqueue_styles filter rather than the WOOCOMMERCE_USE_CSS constant.' ); - - if ( ! WOOCOMMERCE_USE_CSS ) - return false; - } - - return $styles; - } } WC_Frontend_Scripts::init(); diff --git a/includes/class-wc-geo-ip.php b/includes/class-wc-geo-ip.php new file mode 100644 index 00000000000..970edad6cd6 --- /dev/null +++ b/includes/class-wc-geo-ip.php @@ -0,0 +1,1813 @@ +log( $level, $message, array( 'source' => 'geoip' ) ); + } + + /** + * Open geoip file. + * + * @param string $filename + * @param int $flags + */ + public function geoip_open( $filename, $flags ) { + $this->flags = $flags; + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $this->shmid = @shmop_open( self::GEOIP_SHM_KEY, 'a', 0, 0 ); + } else { + if ( $this->filehandle = fopen( $filename, 'rb' ) ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $s_array = fstat( $this->filehandle ); + $this->memory_buffer = fread( $this->filehandle, $s_array['size'] ); + } + } else { + $this->log( 'GeoIP API: Can not open ' . $filename, 'error' ); + } + } + + $this->_setup_segments(); + } + + /** + * Setup segments. + * + * @return WC_Geo_IP instance + */ + private function _setup_segments() { + $this->databaseType = self::GEOIP_COUNTRY_EDITION; + $this->record_length = self::STANDARD_RECORD_LENGTH; + + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $offset = @shmop_size( $this->shmid ) - 3; + + for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { + $delim = @shmop_read( $this->shmid, $offset, 3 ); + $offset += 3; + + if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { + $this->databaseType = ord( @shmop_read( $this->shmid, $offset, 1 ) ); + + if ( $this->databaseType >= 106 ) { + $this->databaseType -= 105; + } + + $offset++; + + if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; + } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; + } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) + || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) + ) { + $this->databaseSegments = 0; + $buf = @shmop_read( $this->shmid, $offset, self::SEGMENT_RECORD_LENGTH ); + + for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { + $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); + } + + if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + ) { + $this->record_length = self::ORG_RECORD_LENGTH; + } + } + + break; + } else { + $offset -= 4; + } + } + if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) + || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) + ) { + $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; + } + } else { + $filepos = ftell( $this->filehandle ); + fseek( $this->filehandle, -3, SEEK_END ); + + for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { + + $delim = fread( $this->filehandle, 3 ); + if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { + + $this->databaseType = ord( fread( $this->filehandle, 1 ) ); + if ( $this->databaseType >= 106 ) { + $this->databaseType -= 105; + } + + if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; + } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; + } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) + || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) + ) { + $this->databaseSegments = 0; + $buf = fread( $this->filehandle, self::SEGMENT_RECORD_LENGTH ); + + for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { + $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); + } + + if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + ) { + $this->record_length = self::ORG_RECORD_LENGTH; + } + } + + break; + } else { + fseek( $this->filehandle, -4, SEEK_CUR ); + } + } + + if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) + || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) + ) { + $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; + } + + fseek( $this->filehandle, $filepos, SEEK_SET ); + } + + return $this; + } + + /** + * Close geoip file. + * + * @return bool + */ + public function geoip_close() { + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + return true; + } + + return fclose( $this->filehandle ); + } + + /** + * Common get record. + * + * @param string $seek_country + * @return WC_Geo_IP_Record instance + */ + private function _common_get_record( $seek_country ) { + // workaround php's broken substr, strpos, etc handling with + // mbstring.func_overload and mbstring.internal_encoding + $mbExists = extension_loaded( 'mbstring' ); + if ( $mbExists ) { + $enc = mb_internal_encoding(); + mb_internal_encoding( 'ISO-8859-1' ); + } + + $record_pointer = $seek_country + ( 2 * $this->record_length - 1 ) * $this->databaseSegments; + + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $record_buf = substr( $this->memory_buffer, $record_pointer, FULL_RECORD_LENGTH ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $record_buf = @shmop_read( $this->shmid, $record_pointer, FULL_RECORD_LENGTH ); + } else { + fseek( $this->filehandle, $record_pointer, SEEK_SET ); + $record_buf = fread( $this->filehandle, FULL_RECORD_LENGTH ); + } + + $record = new WC_Geo_IP_Record(); + $record_buf_pos = 0; + $char = ord( substr( $record_buf, $record_buf_pos, 1 ) ); + $record->country_code = $this->GEOIP_COUNTRY_CODES[ $char ]; + $record->country_code3 = $this->GEOIP_COUNTRY_CODES3[ $char ]; + $record->country_name = $this->GEOIP_COUNTRY_NAMES[ $char ]; + $record->continent_code = $this->GEOIP_CONTINENT_CODES[ $char ]; + $str_length = 0; + + $record_buf_pos++; + + // Get region + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->region = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + $str_length = 0; + + // Get city + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->city = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + $str_length = 0; + + // Get postal code + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->postal_code = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + + // Get latitude and longitude + $latitude = 0; + $longitude = 0; + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $latitude += ( $char << ( $j * 8 ) ); + } + + $record->latitude = ( $latitude / 10000 ) - 180; + + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $longitude += ( $char << ( $j * 8 ) ); + } + + $record->longitude = ( $longitude / 10000 ) - 180; + + if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { + $metroarea_combo = 0; + if ( 'US' === $record->country_code ) { + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $metroarea_combo += ( $char << ( $j * 8 ) ); + } + + $record->metro_code = $record->dma_code = floor( $metroarea_combo / 1000 ); + $record->area_code = $metroarea_combo % 1000; + } + } + + if ( $mbExists ) { + mb_internal_encoding( $enc ); + } + + return $record; + } + + /** + * Get record. + * + * @param int $ipnum + * @return WC_Geo_IP_Record instance + */ + private function _get_record( $ipnum ) { + $seek_country = $this->_geoip_seek_country( $ipnum ); + if ( $seek_country == $this->databaseSegments ) { + return null; + } + + return $this->_common_get_record( $seek_country ); + } + + /** + * Seek country IPv6. + * + * @param int $ipnum + * @return string + */ + public function _geoip_seek_country_v6( $ipnum ) { + // arrays from unpack start with offset 1 + // yet another php mystery. array_merge work around + // this broken behaviour + $v6vec = array_merge( unpack( 'C16', $ipnum ) ); + + $offset = 0; + for ( $depth = 127; $depth >= 0; --$depth ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $buf = $this->_safe_substr( + $this->memory_buffer, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $buf = @shmop_read( + $this->shmid, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } else { + if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { + break; + } + + $buf = fread( $this->filehandle, 2 * $this->record_length ); + } + $x = array( 0, 0 ); + for ( $i = 0; $i < 2; ++$i ) { + for ( $j = 0; $j < $this->record_length; ++$j ) { + $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); + } + } + + $bnum = 127 - $depth; + $idx = $bnum >> 3; + $b_mask = 1 << ( $bnum & 7 ^ 7 ); + if ( ( $v6vec[ $idx ] & $b_mask ) > 0 ) { + if ( $x[1] >= $this->databaseSegments ) { + return $x[1]; + } + $offset = $x[1]; + } else { + if ( $x[0] >= $this->databaseSegments ) { + return $x[0]; + } + $offset = $x[0]; + } + } + + $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); + + return false; + } + + /** + * Seek country. + * + * @param int $ipnum + * @return string + */ + private function _geoip_seek_country( $ipnum ) { + $offset = 0; + for ( $depth = 31; $depth >= 0; --$depth ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $buf = $this->_safe_substr( + $this->memory_buffer, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $buf = @shmop_read( + $this->shmid, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } else { + if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { + break; + } + + $buf = fread( $this->filehandle, 2 * $this->record_length ); + } + + $x = array( 0, 0 ); + for ( $i = 0; $i < 2; ++$i ) { + for ( $j = 0; $j < $this->record_length; ++$j ) { + $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); + } + } + if ( $ipnum & ( 1 << $depth ) ) { + if ( $x[1] >= $this->databaseSegments ) { + return $x[1]; + } + + $offset = $x[1]; + } else { + if ( $x[0] >= $this->databaseSegments ) { + return $x[0]; + } + + $offset = $x[0]; + } + } + + $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); + + return false; + } + + /** + * Record by addr. + * + * @param string $addr + * + * @return WC_Geo_IP_Record + */ + public function geoip_record_by_addr( $addr ) { + if ( null == $addr ) { + return 0; + } + + $ipnum = ip2long( $addr ); + return $this->_get_record( $ipnum ); + } + + /** + * Country ID by addr IPv6. + * + * @param string $addr + * @return int|bool + */ + public function geoip_country_id_by_addr_v6( $addr ) { + if ( ! defined( 'AF_INET6' ) ) { + $this->log( 'GEOIP (geoip_country_id_by_addr_v6): PHP was compiled with --disable-ipv6 option' ); + return false; + } + $ipnum = inet_pton( $addr ); + return $this->_geoip_seek_country_v6( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; + } + + /** + * Country ID by addr. + * + * @param string $addr + * @return int + */ + public function geoip_country_id_by_addr( $addr ) { + $ipnum = ip2long( $addr ); + return $this->_geoip_seek_country( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; + } + + /** + * Country code by addr IPv6. + * + * @param string $addr + * @return string + */ + public function geoip_country_code_by_addr_v6( $addr ) { + $country_id = $this->geoip_country_id_by_addr_v6( $addr ); + if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { + return $this->GEOIP_COUNTRY_CODES[ $country_id ]; + } + + return false; + } + + /** + * Country code by addr. + * + * @param string $addr + * @return string + */ + public function geoip_country_code_by_addr( $addr ) { + if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { + $record = $this->geoip_record_by_addr( $addr ); + if ( false !== $record ) { + return $record->country_code; + } + } else { + $country_id = $this->geoip_country_id_by_addr( $addr ); + if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { + return $this->GEOIP_COUNTRY_CODES[ $country_id ]; + } + } + + return false; + } + + /** + * Encode string. + * + * @param string $string + * @param int $start + * @param int $length + * @return string + */ + private function _safe_substr( $string, $start, $length ) { + // workaround php's broken substr, strpos, etc handling with + // mbstring.func_overload and mbstring.internal_encoding + $mb_exists = extension_loaded( 'mbstring' ); + + if ( $mb_exists ) { + $enc = mb_internal_encoding(); + mb_internal_encoding( 'ISO-8859-1' ); + } + + $buf = substr( $string, $start, $length ); + + if ( $mb_exists ) { + mb_internal_encoding( $enc ); + } + + return $buf; + } +} + +/** + * Geo IP Record class. + */ +class WC_Geo_IP_Record { + + /** + * Country code. + * + * @var string + */ + public $country_code; + + /** + * 3 letters country code. + * + * @var string + */ + public $country_code3; + + /** + * Country name. + * + * @var string + */ + public $country_name; + + /** + * Region. + * + * @var string + */ + public $region; + + /** + * City. + * + * @var string + */ + public $city; + + /** + * Postal code. + * + * @var string + */ + public $postal_code; + + /** + * Latitude + * + * @var int + */ + public $latitude; + + /** + * Longitude. + * + * @var int + */ + public $longitude; + + /** + * Area code. + * + * @var int + */ + public $area_code; + + /** + * DMA Code. + * + * Metro and DMA code are the same. + * Use metro code instead. + * + * @var float + */ + public $dma_code; + + /** + * Metro code. + * + * @var float + */ + public $metro_code; + + /** + * Continent code. + * + * @var string + */ + public $continent_code; +} diff --git a/includes/class-wc-geolocation.php b/includes/class-wc-geolocation.php new file mode 100644 index 00000000000..3ee4fa27cbe --- /dev/null +++ b/includes/class-wc-geolocation.php @@ -0,0 +1,344 @@ + 'http://icanhazip.com', + 'ipify' => 'http://api.ipify.org/', + 'ipecho' => 'http://ipecho.net/plain', + 'ident' => 'http://ident.me', + 'whatismyipaddress' => 'http://bot.whatismyipaddress.com', + 'ip.appspot' => 'http://ip.appspot.com', + ); + + /** @var array API endpoints for geolocating an IP address */ + private static $geoip_apis = array( + 'freegeoip' => 'https://freegeoip.net/json/%s', + 'ipinfo.io' => 'https://ipinfo.io/%s/json', + 'ip-api.com' => 'http://ip-api.com/json/%s', + ); + + /** + * Hook in tabs. + */ + public static function init() { + // Only download the database from MaxMind if the geolocation function is enabled, or a plugin specifically requests it + if ( 'geolocation' === get_option( 'woocommerce_default_customer_address' ) || apply_filters( 'woocommerce_geolocation_update_database_periodically', false ) ) { + add_action( 'woocommerce_geoip_updater', array( __CLASS__, 'update_database' ) ); + } + add_filter( 'pre_update_option_woocommerce_default_customer_address', array( __CLASS__, 'maybe_update_database' ), 10, 2 ); + } + + /** + * Maybe trigger a DB update for the first time. + * @param string $new_value + * @param string $old_value + * @return string + */ + public static function maybe_update_database( $new_value, $old_value ) { + if ( $new_value !== $old_value && 'geolocation' === $new_value ) { + self::update_database(); + } + return $new_value; + } + + /** + * Check if is a valid IP address. + * + * @since 3.0.6 + * @param string $ip_address IP address. + * @return string|bool The valid IP address, otherwise false. + */ + private static function is_ip_address( $ip_address ) { + // WP 4.7+ only. + if ( function_exists( 'rest_is_ip_address' ) ) { + return rest_is_ip_address( $ip_address ); + } + + // Support for WordPress 4.4 to 4.6. + if ( ! class_exists( 'Requests_IPv6', false ) ) { + include_once( dirname( __FILE__ ) . '/vendor/class-requests-ipv6.php' ); + } + + $ipv4_pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/'; + + if ( ! preg_match( $ipv4_pattern, $ip_address ) && ! Requests_IPv6::check_ipv6( $ip_address ) ) { + return false; + } + + return $ip_address; + } + + /** + * Get current user IP Address. + * @return string + */ + public static function get_ip_address() { + if ( isset( $_SERVER['X-Real-IP'] ) ) { + return $_SERVER['X-Real-IP']; + } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + // Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2 + // Make sure we always only send through the first IP in the list which should always be the client IP. + return (string) self::is_ip_address( trim( current( explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ); + } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) { + return $_SERVER['REMOTE_ADDR']; + } + return ''; + } + + /** + * Get user IP Address using an external service. + * This is used mainly as a fallback for users on localhost where + * get_ip_address() will be a local IP and non-geolocatable. + * @return string + */ + public static function get_external_ip_address() { + $external_ip_address = '0.0.0.0'; + + if ( '' !== self::get_ip_address() ) { + $transient_name = 'external_ip_address_' . self::get_ip_address(); + $external_ip_address = get_transient( $transient_name ); + } + + if ( false === $external_ip_address ) { + $external_ip_address = '0.0.0.0'; + $ip_lookup_services = apply_filters( 'woocommerce_geolocation_ip_lookup_apis', self::$ip_lookup_apis ); + $ip_lookup_services_keys = array_keys( $ip_lookup_services ); + shuffle( $ip_lookup_services_keys ); + + foreach ( $ip_lookup_services_keys as $service_name ) { + $service_endpoint = $ip_lookup_services[ $service_name ]; + $response = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) ); + + if ( ! is_wp_error( $response ) && $response['body'] ) { + $external_ip_address = apply_filters( 'woocommerce_geolocation_ip_lookup_api_response', wc_clean( $response['body'] ), $service_name ); + break; + } + } + + set_transient( $transient_name, $external_ip_address, WEEK_IN_SECONDS ); + } + + return $external_ip_address; + } + + /** + * Geolocate an IP address. + * @param string $ip_address + * @param bool $fallback If true, fallbacks to alternative IP detection (can be slower). + * @param bool $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower). + * @return array + */ + public static function geolocate_ip( $ip_address = '', $fallback = true, $api_fallback = true ) { + // Filter to allow custom geolocation of the IP address. + $country_code = apply_filters( 'woocommerce_geolocate_ip', false, $ip_address, $fallback, $api_fallback ); + + if ( false === $country_code ) { + // If GEOIP is enabled in CloudFlare, we can use that (Settings -> CloudFlare Settings -> Settings Overview) + if ( ! empty( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) { + $country_code = sanitize_text_field( strtoupper( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ); + // WP.com VIP has a variable available. + } elseif ( ! empty( $_SERVER['GEOIP_COUNTRY_CODE'] ) ) { + $country_code = sanitize_text_field( strtoupper( $_SERVER['GEOIP_COUNTRY_CODE'] ) ); + // VIP Go has a variable available also. + } elseif ( ! empty( $_SERVER['HTTP_X_COUNTRY_CODE'] ) ) { + $country_code = sanitize_text_field( strtoupper( $_SERVER['HTTP_X_COUNTRY_CODE'] ) ); + } else { + $ip_address = $ip_address ? $ip_address : self::get_ip_address(); + + if ( self::is_IPv6( $ip_address ) ) { + $database = self::get_local_database_path( 'v6' ); + } else { + $database = self::get_local_database_path(); + } + + if ( file_exists( $database ) ) { + $country_code = self::geolocate_via_db( $ip_address ); + } elseif ( $api_fallback ) { + $country_code = self::geolocate_via_api( $ip_address ); + } else { + $country_code = ''; + } + + if ( ! $country_code && $fallback ) { + // May be a local environment - find external IP + return self::geolocate_ip( self::get_external_ip_address(), false, $api_fallback ); + } + } + } + + return array( + 'country' => $country_code, + 'state' => '', + ); + } + + /** + * Path to our local db. + * @param string $version + * @return string + */ + public static function get_local_database_path( $version = 'v4' ) { + $version = ( 'v4' == $version ) ? '' : 'v6'; + $upload_dir = wp_upload_dir(); + + return apply_filters( 'woocommerce_geolocation_local_database_path', $upload_dir['basedir'] . '/GeoIP' . $version . '.dat', $version ); + } + + /** + * Update geoip database. Adapted from https://wordpress.org/plugins/geoip-detect/. + */ + public static function update_database() { + $logger = wc_get_logger(); + + if ( ! is_callable( 'gzopen' ) ) { + $logger->notice( 'Server does not support gzopen', array( 'source' => 'geolocation' ) ); + return; + } + + require_once( ABSPATH . 'wp-admin/includes/file.php' ); + + $tmp_databases = array( + 'v4' => download_url( self::GEOLITE_DB ), + 'v6' => download_url( self::GEOLITE_IPV6_DB ), + ); + + foreach ( $tmp_databases as $tmp_database_version => $tmp_database_path ) { + if ( ! is_wp_error( $tmp_database_path ) ) { + $gzhandle = @gzopen( $tmp_database_path, 'r' ); + $handle = @fopen( self::get_local_database_path( $tmp_database_version ), 'w' ); + + if ( $gzhandle && $handle ) { + while ( $string = gzread( $gzhandle, 4096 ) ) { + fwrite( $handle, $string, strlen( $string ) ); + } + gzclose( $gzhandle ); + fclose( $handle ); + } else { + $logger->notice( 'Unable to open database file', array( 'source' => 'geolocation' ) ); + } + @unlink( $tmp_database_path ); + } else { + $logger->notice( + 'Unable to download GeoIP Database: ' . $tmp_database_path->get_error_message(), + array( 'source' => 'geolocation' ) + ); + } + } + } + + /** + * Use MAXMIND GeoLite database to geolocation the user. + * @param string $ip_address + * @return string + */ + private static function geolocate_via_db( $ip_address ) { + if ( ! class_exists( 'WC_Geo_IP', false ) ) { + include_once( WC_ABSPATH . 'includes/class-wc-geo-ip.php' ); + } + + $gi = new WC_Geo_IP(); + + if ( self::is_IPv6( $ip_address ) ) { + $database = self::get_local_database_path( 'v6' ); + $gi->geoip_open( $database, 0 ); + $country_code = $gi->geoip_country_code_by_addr_v6( $ip_address ); + } else { + $database = self::get_local_database_path(); + $gi->geoip_open( $database, 0 ); + $country_code = $gi->geoip_country_code_by_addr( $ip_address ); + } + + $gi->geoip_close(); + + return sanitize_text_field( strtoupper( $country_code ) ); + } + + /** + * Use APIs to Geolocate the user. + * @param string $ip_address + * @return string|bool + */ + private static function geolocate_via_api( $ip_address ) { + $country_code = get_transient( 'geoip_' . $ip_address ); + + if ( false === $country_code ) { + $geoip_services = apply_filters( 'woocommerce_geolocation_geoip_apis', self::$geoip_apis ); + $geoip_services_keys = array_keys( $geoip_services ); + shuffle( $geoip_services_keys ); + + foreach ( $geoip_services_keys as $service_name ) { + $service_endpoint = $geoip_services[ $service_name ]; + $response = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) ); + + if ( ! is_wp_error( $response ) && $response['body'] ) { + switch ( $service_name ) { + case 'ipinfo.io' : + $data = json_decode( $response['body'] ); + $country_code = isset( $data->country ) ? $data->country : ''; + break; + case 'ip-api.com' : + $data = json_decode( $response['body'] ); + $country_code = isset( $data->countryCode ) ? $data->countryCode : ''; + break; + case 'freegeoip' : + $data = json_decode( $response['body'] ); + $country_code = isset( $data->country_code ) ? $data->country_code : ''; + break; + default : + $country_code = apply_filters( 'woocommerce_geolocation_geoip_response_' . $service_name, '', $response['body'] ); + break; + } + + $country_code = sanitize_text_field( strtoupper( $country_code ) ); + + if ( $country_code ) { + break; + } + } + } + + set_transient( 'geoip_' . $ip_address, $country_code, WEEK_IN_SECONDS ); + } + + return $country_code; + } + + /** + * Test if is IPv6. + * + * @since 2.4.0 + * + * @param string $ip_address + * @return bool + */ + private static function is_IPv6( $ip_address ) { + return false !== filter_var( $ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ); + } +} + +WC_Geolocation::init(); diff --git a/includes/class-wc-https.php b/includes/class-wc-https.php index 0e90053a585..de3e6f4ce85 100644 --- a/includes/class-wc-https.php +++ b/includes/class-wc-https.php @@ -7,11 +7,11 @@ if ( ! defined( 'ABSPATH' ) ) { /** * WC_HTTPS class. * - * @class WC_HTTPS - * @version 2.2.0 - * @package WooCommerce/Classes - * @category Class - * @author WooThemes + * @class WC_HTTPS + * @version 2.2.0 + * @package WooCommerce/Classes + * @category Class + * @author WooThemes */ class WC_HTTPS { @@ -19,59 +19,71 @@ class WC_HTTPS { * Hook in our HTTPS functions if we're on the frontend. This will ensure any links output to a page (when viewing via HTTPS) are also served over HTTPS. */ public static function init() { - if ( 'yes' == get_option( 'woocommerce_force_ssl_checkout' ) ) { - if ( ! is_admin() || ( defined( 'DOING_AJAX' ) && in_array( $_REQUEST['action'], array( 'woocommerce_get_refreshed_fragments', 'woocommerce_checkout', 'woocommerce_update_order_review', 'woocommerce_update_shipping_method', 'woocommerce_apply_coupon' ) ) ) ) { - // HTTPS urls with SSL on - $filters = array( 'post_thumbnail_html', 'wp_get_attachment_url', 'wp_get_attachment_image_attributes', 'wp_get_attachment_url', 'option_stylesheet_url', 'option_template_url', 'script_loader_src', 'style_loader_src', 'template_directory_uri', 'stylesheet_directory_uri', 'site_url' ); + if ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) && ! is_admin() ) { + // HTTPS urls with SSL on + $filters = array( + 'post_thumbnail_html', + 'wp_get_attachment_image_attributes', + 'wp_get_attachment_url', + 'option_stylesheet_url', + 'option_template_url', + 'script_loader_src', + 'style_loader_src', + 'template_directory_uri', + 'stylesheet_directory_uri', + 'site_url', + ); - foreach ( $filters as $filter ) { - add_filter( $filter, array( __CLASS__, 'force_https_url' ) ); - } - - add_filter( 'page_link', array( __CLASS__, 'force_https_page_link' ), 10, 2 ); - add_action( 'template_redirect', array( __CLASS__, 'force_https_template_redirect' ) ); + foreach ( $filters as $filter ) { + add_filter( $filter, array( __CLASS__, 'force_https_url' ), 999 ); + } - if ( get_option('woocommerce_unforce_ssl_checkout') == 'yes' ) { - add_action( 'template_redirect', array( __CLASS__, 'unforce_https_template_redirect' ) ); - } + add_filter( 'page_link', array( __CLASS__, 'force_https_page_link' ), 10, 2 ); + add_action( 'template_redirect', array( __CLASS__, 'force_https_template_redirect' ) ); + + if ( 'yes' == get_option( 'woocommerce_unforce_ssl_checkout' ) ) { + add_action( 'template_redirect', array( __CLASS__, 'unforce_https_template_redirect' ) ); } } + add_action( 'http_api_curl', array( __CLASS__, 'http_api_curl' ), 10, 3 ); } /** - * force_https_url function. + * Force https for urls. * * @param mixed $content * @return string */ public static function force_https_url( $content ) { if ( is_ssl() ) { - if ( is_array( $content ) ) + if ( is_array( $content ) ) { $content = array_map( 'WC_HTTPS::force_https_url', $content ); - else + } else { $content = str_replace( 'http:', 'https:', $content ); + } } return $content; } /** - * Force a post link to be SSL if needed + * Force a post link to be SSL if needed. + * + * @param string $link + * @param int $page_id * - * @param string $post_link - * @param object $post * @return string */ public static function force_https_page_link( $link, $page_id ) { if ( in_array( $page_id, array( get_option( 'woocommerce_checkout_page_id' ), get_option( 'woocommerce_myaccount_page_id' ) ) ) ) { $link = str_replace( 'http:', 'https:', $link ); - } elseif ( get_option('woocommerce_unforce_ssl_checkout') == 'yes' ) { + } elseif ( 'yes' === get_option( 'woocommerce_unforce_ssl_checkout' ) && ! wc_site_is_https() ) { $link = str_replace( 'https:', 'http:', $link ); } return $link; } /** - * Template redirect - if we end up on a page ensure it has the correct http/https url + * Template redirect - if we end up on a page ensure it has the correct http/https url. */ public static function force_https_template_redirect() { if ( ! is_ssl() && ( is_checkout() || is_account_page() || apply_filters( 'woocommerce_force_ssl_checkout', false ) ) ) { @@ -87,10 +99,14 @@ class WC_HTTPS { } /** - * Template redirect - if we end up on a page ensure it has the correct http/https url + * Template redirect - if we end up on a page ensure it has the correct http/https url. */ public static function unforce_https_template_redirect() { - if ( is_ssl() && $_SERVER['REQUEST_URI'] && ! is_checkout() && ! is_ajax() && ! is_account_page() && apply_filters( 'woocommerce_unforce_ssl_checkout', true ) ) { + if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) { + return; + } + + if ( ! wc_site_is_https() && is_ssl() && $_SERVER['REQUEST_URI'] && ! is_checkout() && ! is_ajax() && ! is_account_page() && apply_filters( 'woocommerce_unforce_ssl_checkout', true ) ) { if ( 0 === strpos( $_SERVER['REQUEST_URI'], 'http' ) ) { wp_safe_redirect( preg_replace( '|^https://|', 'http://', $_SERVER['REQUEST_URI'] ) ); @@ -101,6 +117,22 @@ class WC_HTTPS { } } } + + /** + * Force posts to PayPal to use TLS v1.2. See: + * https://core.trac.wordpress.org/ticket/36320 + * https://core.trac.wordpress.org/ticket/34924#comment:13 + * https://www.paypal-knowledge.com/infocenter/index?page=content&widgetview=true&id=FAQ1914&viewlocale=en_US + * + * @param string $handle + * @param mixed $r + * @param string $url + */ + public static function http_api_curl( $handle, $r, $url ) { + if ( strstr( $url, 'https://' ) && ( strstr( $url, '.paypal.com/nvp' ) || strstr( $url, '.paypal.com/cgi-bin/webscr' ) ) ) { + curl_setopt( $handle, CURLOPT_SSLVERSION, 6 ); + } + } } WC_HTTPS::init(); diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php index 62e8a4aa6cc..0ee7fcb15a4 100644 --- a/includes/class-wc-install.php +++ b/includes/class-wc-install.php @@ -2,300 +2,414 @@ /** * Installation related functions and actions. * - * @author WooThemes - * @category Admin - * @package WooCommerce/Classes - * @version 2.1.0 + * @author WooThemes + * @category Admin + * @package WooCommerce/Classes + * @version 3.0.0 */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly - -if ( ! class_exists( 'WC_Install' ) ) : +if ( ! defined( 'ABSPATH' ) ) { + exit; +} /** - * WC_Install Class + * WC_Install Class. */ class WC_Install { + /** @var array DB updates and callbacks that need to be run per version */ + private static $db_updates = array( + '2.0.0' => array( + 'wc_update_200_file_paths', + 'wc_update_200_permalinks', + 'wc_update_200_subcat_display', + 'wc_update_200_taxrates', + 'wc_update_200_line_items', + 'wc_update_200_images', + 'wc_update_200_db_version', + ), + '2.0.9' => array( + 'wc_update_209_brazillian_state', + 'wc_update_209_db_version', + ), + '2.1.0' => array( + 'wc_update_210_remove_pages', + 'wc_update_210_file_paths', + 'wc_update_210_db_version', + ), + '2.2.0' => array( + 'wc_update_220_shipping', + 'wc_update_220_order_status', + 'wc_update_220_variations', + 'wc_update_220_attributes', + 'wc_update_220_db_version', + ), + '2.3.0' => array( + 'wc_update_230_options', + 'wc_update_230_db_version', + ), + '2.4.0' => array( + 'wc_update_240_options', + 'wc_update_240_shipping_methods', + 'wc_update_240_api_keys', + 'wc_update_240_webhooks', + 'wc_update_240_refunds', + 'wc_update_240_db_version', + ), + '2.4.1' => array( + 'wc_update_241_variations', + 'wc_update_241_db_version', + ), + '2.5.0' => array( + 'wc_update_250_currency', + 'wc_update_250_db_version', + ), + '2.6.0' => array( + 'wc_update_260_options', + 'wc_update_260_termmeta', + 'wc_update_260_zones', + 'wc_update_260_zone_methods', + 'wc_update_260_refunds', + 'wc_update_260_db_version', + ), + '3.0.0' => array( + 'wc_update_300_webhooks', + 'wc_update_300_grouped_products', + 'wc_update_300_settings', + 'wc_update_300_product_visibility', + 'wc_update_300_db_version', + ), + '3.1.0' => array( + 'wc_update_310_downloadable_products', + 'wc_update_310_old_comments', + 'wc_update_310_db_version', + ), + '3.2.0' => array( + 'wc_update_320_mexican_states', + 'wc_update_320_db_version', + ), + ); + + /** @var object Background update class */ + private static $background_updater; + /** * Hook in tabs. */ - public function __construct() { - register_activation_hook( WC_PLUGIN_FILE, array( $this, 'install' ) ); - - add_action( 'admin_init', array( $this, 'install_actions' ) ); - add_action( 'admin_init', array( $this, 'check_version' ), 5 ); - add_action( 'in_plugin_update_message-woocommerce/woocommerce.php', array( $this, 'in_plugin_update_message' ) ); + public static function init() { + add_action( 'init', array( __CLASS__, 'check_version' ), 5 ); + add_action( 'init', array( __CLASS__, 'init_background_updater' ), 5 ); + add_action( 'admin_init', array( __CLASS__, 'install_actions' ) ); + add_action( 'in_plugin_update_message-woocommerce/woocommerce.php', array( __CLASS__, 'in_plugin_update_message' ) ); + add_filter( 'plugin_action_links_' . WC_PLUGIN_BASENAME, array( __CLASS__, 'plugin_action_links' ) ); + add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 ); + add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) ); + add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) ); + add_action( 'woocommerce_plugin_background_installer', array( __CLASS__, 'background_installer' ), 10, 2 ); + add_action( 'woocommerce_theme_background_installer', array( __CLASS__, 'theme_background_installer' ), 10, 1 ); } /** - * check_version function. - * - * @access public - * @return void + * Init background updates */ - public function check_version() { - if ( ! defined( 'IFRAME_REQUEST' ) && ( get_option( 'woocommerce_version' ) != WC()->version || get_option( 'woocommerce_db_version' ) != WC()->version ) ) { - $this->install(); + public static function init_background_updater() { + include_once( dirname( __FILE__ ) . '/class-wc-background-updater.php' ); + self::$background_updater = new WC_Background_Updater(); + } + /** + * Check WooCommerce version and run the updater is required. + * + * This check is done on all requests and runs if the versions do not match. + */ + public static function check_version() { + if ( ! defined( 'IFRAME_REQUEST' ) && get_option( 'woocommerce_version' ) !== WC()->version ) { + self::install(); do_action( 'woocommerce_updated' ); } } /** - * Install actions such as installing pages when a button is clicked. + * Install actions when a update button is clicked within the admin area. + * + * This function is hooked into admin_init to affect admin only. */ - public function install_actions() { - // Install - Add pages button - if ( ! empty( $_GET['install_woocommerce_pages'] ) ) { - - self::create_pages(); - - // We no longer need to install pages - delete_option( '_wc_needs_pages' ); - delete_transient( '_wc_activation_redirect' ); - - // What's new redirect - wp_redirect( admin_url( 'index.php?page=wc-about&wc-installed=true' ) ); - exit; - - // Skip button - } elseif ( ! empty( $_GET['skip_install_woocommerce_pages'] ) ) { - - // We no longer need to install pages - delete_option( '_wc_needs_pages' ); - delete_transient( '_wc_activation_redirect' ); - - // What's new redirect - wp_redirect( admin_url( 'index.php?page=wc-about' ) ); - exit; - - // Update button - } elseif ( ! empty( $_GET['do_update_woocommerce'] ) ) { - - $this->update(); - - // Update complete - delete_option( '_wc_needs_pages' ); - delete_option( '_wc_needs_update' ); - delete_transient( '_wc_activation_redirect' ); - - // What's new redirect - wp_redirect( admin_url( 'index.php?page=wc-about&wc-updated=true' ) ); + public static function install_actions() { + if ( ! empty( $_GET['do_update_woocommerce'] ) ) { + self::update(); + WC_Admin_Notices::add_notice( 'update' ); + } + if ( ! empty( $_GET['force_update_woocommerce'] ) ) { + do_action( 'wp_wc_updater_cron' ); + wp_safe_redirect( admin_url( 'admin.php?page=wc-settings' ) ); exit; } } /** - * Install WC + * Install WC. */ - public function install() { - $this->create_options(); - $this->create_tables(); - $this->create_roles(); + public static function install() { + global $wpdb; + + if ( ! is_blog_installed() ) { + return; + } + + if ( ! defined( 'WC_INSTALLING' ) ) { + define( 'WC_INSTALLING', true ); + } + + // Ensure needed classes are loaded + include_once( dirname( __FILE__ ) . '/admin/class-wc-admin-notices.php' ); + + self::create_options(); + self::create_tables(); + self::create_roles(); // Register post types - include_once( 'class-wc-post-types.php' ); WC_Post_types::register_post_types(); WC_Post_types::register_taxonomies(); // Also register endpoints - this needs to be done prior to rewrite rule flush WC()->query->init_query_vars(); WC()->query->add_endpoints(); + WC_API::add_endpoint(); + WC_Auth::add_endpoint(); - $this->create_terms(); - $this->create_cron_jobs(); - $this->create_files(); - $this->create_css_from_less(); + self::create_terms(); + self::create_cron_jobs(); + self::create_files(); - // Queue upgrades - $current_version = get_option( 'woocommerce_version', null ); - $current_db_version = get_option( 'woocommerce_db_version', null ); + // Queue upgrades/setup wizard + $current_wc_version = get_option( 'woocommerce_version', null ); + $current_db_version = get_option( 'woocommerce_db_version', null ); - if ( version_compare( $current_db_version, '2.2.0', '<' ) && null !== $current_db_version ) { - update_option( '_wc_needs_update', 1 ); + WC_Admin_Notices::remove_all_notices(); + + // No versions? This is a new install :) + if ( is_null( $current_wc_version ) && is_null( $current_db_version ) && apply_filters( 'woocommerce_enable_setup_wizard', true ) ) { + WC_Admin_Notices::add_notice( 'install' ); + set_transient( '_wc_activation_redirect', 1, 30 ); + + // No page? Let user run wizard again.. + } elseif ( ! get_option( 'woocommerce_cart_page_id' ) ) { + WC_Admin_Notices::add_notice( 'install' ); + } + + if ( ! is_null( $current_db_version ) && version_compare( $current_db_version, max( array_keys( self::$db_updates ) ), '<' ) ) { + WC_Admin_Notices::add_notice( 'update' ); } else { - update_option( 'woocommerce_db_version', WC()->version ); + self::update_db_version(); } - // Update version - update_option( 'woocommerce_version', WC()->version ); - - // Check if pages are needed - if ( wc_get_page_id( 'shop' ) < 1 ) { - update_option( '_wc_needs_pages', 1 ); - } + self::update_wc_version(); // Flush rules after install - flush_rewrite_rules(); + do_action( 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); - // Redirect to welcome screen - set_transient( '_wc_activation_redirect', 1, 60 * 60 ); + /* + * Deletes all expired transients. The multi-table delete syntax is used + * to delete the transient record from table a, and the corresponding + * transient_timeout record from table b. + * + * Based on code inside core's upgrade_network() function. + */ + $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b + WHERE a.option_name LIKE %s + AND a.option_name NOT LIKE %s + AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) + AND b.option_value < %d"; + $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) ); + + // Trigger action + do_action( 'woocommerce_installed' ); } /** - * Handle updates + * Update WC version to current. */ - public function update() { - // Do updates + private static function update_wc_version() { + delete_option( 'woocommerce_version' ); + add_option( 'woocommerce_version', WC()->version ); + } + + /** + * Get list of DB update callbacks. + * + * @since 3.0.0 + * @return array + */ + public static function get_db_update_callbacks() { + return self::$db_updates; + } + + /** + * Push all needed DB updates to the queue for processing. + */ + private static function update() { $current_db_version = get_option( 'woocommerce_db_version' ); + $logger = wc_get_logger(); + $update_queued = false; - if ( version_compare( $current_db_version, '1.4', '<' ) ) { - include( 'updates/woocommerce-update-1.4.php' ); - update_option( 'woocommerce_db_version', '1.4' ); - } - - if ( version_compare( $current_db_version, '1.5', '<' ) ) { - include( 'updates/woocommerce-update-1.5.php' ); - update_option( 'woocommerce_db_version', '1.5' ); - } - - if ( version_compare( $current_db_version, '2.0', '<' ) ) { - include( 'updates/woocommerce-update-2.0.php' ); - update_option( 'woocommerce_db_version', '2.0' ); - } - - if ( version_compare( $current_db_version, '2.0.9', '<' ) ) { - include( 'updates/woocommerce-update-2.0.9.php' ); - update_option( 'woocommerce_db_version', '2.0.9' ); - } - - if ( version_compare( $current_db_version, '2.0.14', '<' ) ) { - if ( 'HU' == get_option( 'woocommerce_default_country' ) ) { - update_option( 'woocommerce_default_country', 'HU:BU' ); + foreach ( self::get_db_update_callbacks() as $version => $update_callbacks ) { + if ( version_compare( $current_db_version, $version, '<' ) ) { + foreach ( $update_callbacks as $update_callback ) { + $logger->info( + sprintf( 'Queuing %s - %s', $version, $update_callback ), + array( 'source' => 'wc_db_updates' ) + ); + self::$background_updater->push_to_queue( $update_callback ); + $update_queued = true; + } } - - update_option( 'woocommerce_db_version', '2.0.14' ); } - if ( version_compare( $current_db_version, '2.1.0', '<' ) || WC_VERSION == '2.1-bleeding' ) { - include( 'updates/woocommerce-update-2.1.php' ); - update_option( 'woocommerce_db_version', '2.1.0' ); + if ( $update_queued ) { + self::$background_updater->save()->dispatch(); } - - if ( version_compare( $current_db_version, '2.2.0', '<' ) || WC_VERSION == '2.2-bleeding' ) { - include( 'updates/woocommerce-update-2.2.php' ); - update_option( 'woocommerce_db_version', '2.2.0' ); - } - - update_option( 'woocommerce_db_version', WC()->version ); } /** - * Create cron jobs (clear them first) + * Update DB version to current. + * @param string $version */ - private function create_cron_jobs() { - // Cron jobs + public static function update_db_version( $version = null ) { + delete_option( 'woocommerce_db_version' ); + add_option( 'woocommerce_db_version', is_null( $version ) ? WC()->version : $version ); + } + + /** + * Add more cron schedules. + * @param array $schedules + * @return array + */ + public static function cron_schedules( $schedules ) { + $schedules['monthly'] = array( + 'interval' => 2635200, + 'display' => __( 'Monthly', 'woocommerce' ), + ); + return $schedules; + } + + /** + * Create cron jobs (clear them first). + */ + private static function create_cron_jobs() { wp_clear_scheduled_hook( 'woocommerce_scheduled_sales' ); wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); wp_clear_scheduled_hook( 'woocommerce_cleanup_sessions' ); + wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); + wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' ); - $ve = get_option( 'gmt_offset' ) > 0 ? '+' : '-'; + $ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+'; wp_schedule_event( strtotime( '00:00 tomorrow ' . $ve . get_option( 'gmt_offset' ) . ' HOURS' ), 'daily', 'woocommerce_scheduled_sales' ); - $held_duration = get_option( 'woocommerce_hold_stock_minutes', null ); + $held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' ); - if ( is_null( $held_duration ) ) { - $held_duration = '60'; - } - - if ( $held_duration != '' ) { + if ( '' != $held_duration ) { wp_schedule_single_event( time() + ( absint( $held_duration ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); } wp_schedule_event( time(), 'twicedaily', 'woocommerce_cleanup_sessions' ); + wp_schedule_event( strtotime( 'first tuesday of next month' ), 'monthly', 'woocommerce_geoip_updater' ); + wp_schedule_event( time(), apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' ); } /** - * Create pages that the plugin relies on, storing page id's in variables. - * - * @access public - * @return void + * Create pages that the plugin relies on, storing page IDs in variables. */ public static function create_pages() { + include_once( dirname( __FILE__ ) . '/admin/wc-admin-functions.php' ); + $pages = apply_filters( 'woocommerce_create_pages', array( 'shop' => array( 'name' => _x( 'shop', 'Page slug', 'woocommerce' ), 'title' => _x( 'Shop', 'Page title', 'woocommerce' ), - 'content' => '' + 'content' => '', ), 'cart' => array( 'name' => _x( 'cart', 'Page slug', 'woocommerce' ), 'title' => _x( 'Cart', 'Page title', 'woocommerce' ), - 'content' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']' + 'content' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', ), 'checkout' => array( 'name' => _x( 'checkout', 'Page slug', 'woocommerce' ), 'title' => _x( 'Checkout', 'Page title', 'woocommerce' ), - 'content' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']' + 'content' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', ), 'myaccount' => array( 'name' => _x( 'my-account', 'Page slug', 'woocommerce' ), - 'title' => _x( 'My Account', 'Page title', 'woocommerce' ), - 'content' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']' - ) + 'title' => _x( 'My account', 'Page title', 'woocommerce' ), + 'content' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']', + ), ) ); foreach ( $pages as $key => $page ) { wc_create_page( esc_sql( $page['name'] ), 'woocommerce_' . $key . '_page_id', $page['title'], $page['content'], ! empty( $page['parent'] ) ? wc_get_page_id( $page['parent'] ) : '' ); } + + delete_transient( 'woocommerce_cache_excluded_uris' ); } /** - * Add the default terms for WC taxonomies - product types and order statuses. Modify this at your own risk. + * Default options. * - * @access public - * @return void + * Sets up the default options used on the settings page. */ - private function create_terms() { + private static function create_options() { + // Include settings so that we can run through defaults + include_once( dirname( __FILE__ ) . '/admin/class-wc-admin-settings.php' ); - $taxonomies = array( - 'product_type' => array( - 'simple', - 'grouped', - 'variable', - 'external' - ) - ); + $settings = WC_Admin_Settings::get_settings_pages(); - foreach ( $taxonomies as $taxonomy => $terms ) { - foreach ( $terms as $term ) { - if ( ! get_term_by( 'slug', sanitize_title( $term ), $taxonomy ) ) { - wp_insert_term( $term, $taxonomy ); + foreach ( $settings as $section ) { + if ( ! method_exists( $section, 'get_settings' ) ) { + continue; + } + $subsections = array_unique( array_merge( array( '' ), array_keys( $section->get_sections() ) ) ); + + foreach ( $subsections as $subsection ) { + foreach ( $section->get_settings( $subsection ) as $value ) { + if ( isset( $value['default'] ) && isset( $value['id'] ) ) { + $autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true; + add_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) ); + } } } } } /** - * Default options - * - * Sets up the default options used on the settings page - * - * @access public + * Add the default terms for WC taxonomies - product types and order statuses. Modify this at your own risk. */ - function create_options() { - // Include settings so that we can run through defaults - include_once( 'admin/class-wc-admin-settings.php' ); + public static function create_terms() { + $taxonomies = array( + 'product_type' => array( + 'simple', + 'grouped', + 'variable', + 'external', + ), + 'product_visibility' => array( + 'exclude-from-search', + 'exclude-from-catalog', + 'featured', + 'outofstock', + 'rated-1', + 'rated-2', + 'rated-3', + 'rated-4', + 'rated-5', + ), + ); - $settings = WC_Admin_Settings::get_settings_pages(); - - foreach ( $settings as $section ) { - foreach ( $section->get_settings() as $value ) { - if ( isset( $value['default'] ) && isset( $value['id'] ) ) { - $autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true; - add_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) ); - } - } - - // Special case to install the inventory settings. - if ( $section instanceof WC_Settings_Products ) { - foreach ( $section->get_settings( 'inventory' ) as $value ) { - if ( isset( $value['default'] ) && isset( $value['id'] ) ) { - $autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true; - add_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) ); - } + foreach ( $taxonomies as $taxonomy => $terms ) { + foreach ( $terms as $term ) { + if ( ! get_term_by( 'name', $term, $taxonomy ) ) { + wp_insert_term( $term, $taxonomy ); } } } @@ -313,214 +427,312 @@ class WC_Install { * woocommerce_order_itemmeta - Order line item meta is stored in a table for storing extra data. * woocommerce_tax_rates - Tax Rates are stored inside 2 tables making tax queries simple and efficient. * woocommerce_tax_rate_locations - Each rate can be applied to more than one postcode/city hence the second table. - * - * @access public - * @return void */ - private function create_tables() { + private static function create_tables() { global $wpdb; $wpdb->hide_errors(); - $collate = ''; - - if ( $wpdb->has_cap( 'collation' ) ) { - if ( ! empty($wpdb->charset ) ) { - $collate .= "DEFAULT CHARACTER SET $wpdb->charset"; - } - if ( ! empty($wpdb->collate ) ) { - $collate .= " COLLATE $wpdb->collate"; - } - } - require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); /** - * Update schemas before DBDELTA - * - * Before updating, remove any primary keys which could be modified due to schema updates + * Before updating with DBDELTA, remove any primary keys which could be + * modified due to schema updates. */ if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_downloadable_product_permissions';" ) ) { if ( ! $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_downloadable_product_permissions` LIKE 'permission_id';" ) ) { - $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` bigint(20) NOT NULL PRIMARY KEY AUTO_INCREMENT;" ); + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT;" ); } } - // WooCommerce Tables - $woocommerce_tables = " - CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies ( - attribute_id bigint(20) NOT NULL auto_increment, - attribute_name varchar(200) NOT NULL, - attribute_label longtext NULL, - attribute_type varchar(200) NOT NULL, - attribute_orderby varchar(200) NOT NULL, - PRIMARY KEY (attribute_id), - KEY attribute_name (attribute_name) - ) $collate; - CREATE TABLE {$wpdb->prefix}woocommerce_termmeta ( - meta_id bigint(20) NOT NULL auto_increment, - woocommerce_term_id bigint(20) NOT NULL, - meta_key varchar(255) NULL, - meta_value longtext NULL, - PRIMARY KEY (meta_id), - KEY woocommerce_term_id (woocommerce_term_id), - KEY meta_key (meta_key) - ) $collate; - CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ( - permission_id bigint(20) NOT NULL auto_increment, - download_id varchar(32) NOT NULL, - product_id bigint(20) NOT NULL, - order_id bigint(20) NOT NULL DEFAULT 0, - order_key varchar(200) NOT NULL, - user_email varchar(200) NOT NULL, - user_id bigint(20) NULL, - downloads_remaining varchar(9) NULL, - access_granted datetime NOT NULL default '0000-00-00 00:00:00', - access_expires datetime NULL default null, - download_count bigint(20) NOT NULL DEFAULT 0, - PRIMARY KEY (permission_id), - KEY download_order_key_product (product_id,order_id,order_key,download_id), - KEY download_order_product (download_id,order_id,product_id) - ) $collate; - CREATE TABLE {$wpdb->prefix}woocommerce_order_items ( - order_item_id bigint(20) NOT NULL auto_increment, - order_item_name longtext NOT NULL, - order_item_type varchar(200) NOT NULL DEFAULT '', - order_id bigint(20) NOT NULL, - PRIMARY KEY (order_item_id), - KEY order_id (order_id) - ) $collate; - CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta ( - meta_id bigint(20) NOT NULL auto_increment, - order_item_id bigint(20) NOT NULL, - meta_key varchar(255) NULL, - meta_value longtext NULL, - PRIMARY KEY (meta_id), - KEY order_item_id (order_item_id), - KEY meta_key (meta_key) - ) $collate; - CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates ( - tax_rate_id bigint(20) NOT NULL auto_increment, - tax_rate_country varchar(200) NOT NULL DEFAULT '', - tax_rate_state varchar(200) NOT NULL DEFAULT '', - tax_rate varchar(200) NOT NULL DEFAULT '', - tax_rate_name varchar(200) NOT NULL DEFAULT '', - tax_rate_priority bigint(20) NOT NULL, - tax_rate_compound int(1) NOT NULL DEFAULT 0, - tax_rate_shipping int(1) NOT NULL DEFAULT 1, - tax_rate_order bigint(20) NOT NULL, - tax_rate_class varchar(200) NOT NULL DEFAULT '', - PRIMARY KEY (tax_rate_id), - KEY tax_rate_country (tax_rate_country), - KEY tax_rate_state (tax_rate_state), - KEY tax_rate_class (tax_rate_class), - KEY tax_rate_priority (tax_rate_priority) - ) $collate; - CREATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations ( - location_id bigint(20) NOT NULL auto_increment, - location_code varchar(255) NOT NULL, - tax_rate_id bigint(20) NOT NULL, - location_type varchar(40) NOT NULL, - PRIMARY KEY (location_id), - KEY tax_rate_id (tax_rate_id), - KEY location_type (location_type), - KEY location_type_code (location_type,location_code) - ) $collate; - "; - dbDelta( $woocommerce_tables ); + dbDelta( self::get_schema() ); + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); + + if ( is_null( $index_exists ) ) { + // Add an index to the field comment_type to improve the response time of the query + // used by WC_Comments::wp_count_comments() to get the number of comments by type. + $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); + } } /** - * Create roles and capabilities + * Get Table schema. + * + * https://github.com/woocommerce/woocommerce/wiki/Database-Description/ + * + * A note on indexes; Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. + * As of WordPress 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which + * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. + * + * Changing indexes may cause duplicate index notices in logs due to https://core.trac.wordpress.org/ticket/34870 but dropping + * indexes first causes too much load on some servers/larger DB. + * + * @return string */ - public function create_roles() { + private static function get_schema() { + global $wpdb; + + $collate = ''; + + if ( $wpdb->has_cap( 'collation' ) ) { + $collate = $wpdb->get_charset_collate(); + } + + $tables = " +CREATE TABLE {$wpdb->prefix}woocommerce_sessions ( + session_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + session_key char(32) NOT NULL, + session_value longtext NOT NULL, + session_expiry BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (session_key), + UNIQUE KEY session_id (session_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_api_keys ( + key_id BIGINT UNSIGNED NOT NULL auto_increment, + user_id BIGINT UNSIGNED NOT NULL, + description varchar(200) NULL, + permissions varchar(10) NOT NULL, + consumer_key char(64) NOT NULL, + consumer_secret char(43) NOT NULL, + nonces longtext NULL, + truncated_key char(7) NOT NULL, + last_access datetime NULL default null, + PRIMARY KEY (key_id), + KEY consumer_key (consumer_key), + KEY consumer_secret (consumer_secret) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies ( + attribute_id BIGINT UNSIGNED NOT NULL auto_increment, + attribute_name varchar(200) NOT NULL, + attribute_label varchar(200) NULL, + attribute_type varchar(20) NOT NULL, + attribute_orderby varchar(20) NOT NULL, + attribute_public int(1) NOT NULL DEFAULT 1, + PRIMARY KEY (attribute_id), + KEY attribute_name (attribute_name(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ( + permission_id BIGINT UNSIGNED NOT NULL auto_increment, + download_id varchar(32) NOT NULL, + product_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + order_key varchar(200) NOT NULL, + user_email varchar(200) NOT NULL, + user_id BIGINT UNSIGNED NULL, + downloads_remaining varchar(9) NULL, + access_granted datetime NOT NULL default '0000-00-00 00:00:00', + access_expires datetime NULL default null, + download_count BIGINT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (permission_id), + KEY download_order_key_product (product_id,order_id,order_key(16),download_id), + KEY download_order_product (download_id,order_id,product_id), + KEY order_id (order_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_order_items ( + order_item_id BIGINT UNSIGNED NOT NULL auto_increment, + order_item_name TEXT NOT NULL, + order_item_type varchar(200) NOT NULL DEFAULT '', + order_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (order_item_id), + KEY order_id (order_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta ( + meta_id BIGINT UNSIGNED NOT NULL auto_increment, + order_item_id BIGINT UNSIGNED NOT NULL, + meta_key varchar(255) default NULL, + meta_value longtext NULL, + PRIMARY KEY (meta_id), + KEY order_item_id (order_item_id), + KEY meta_key (meta_key(32)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates ( + tax_rate_id BIGINT UNSIGNED NOT NULL auto_increment, + tax_rate_country varchar(2) NOT NULL DEFAULT '', + tax_rate_state varchar(200) NOT NULL DEFAULT '', + tax_rate varchar(8) NOT NULL DEFAULT '', + tax_rate_name varchar(200) NOT NULL DEFAULT '', + tax_rate_priority BIGINT UNSIGNED NOT NULL, + tax_rate_compound int(1) NOT NULL DEFAULT 0, + tax_rate_shipping int(1) NOT NULL DEFAULT 1, + tax_rate_order BIGINT UNSIGNED NOT NULL, + tax_rate_class varchar(200) NOT NULL DEFAULT '', + PRIMARY KEY (tax_rate_id), + KEY tax_rate_country (tax_rate_country), + KEY tax_rate_state (tax_rate_state(2)), + KEY tax_rate_class (tax_rate_class(10)), + KEY tax_rate_priority (tax_rate_priority) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations ( + location_id BIGINT UNSIGNED NOT NULL auto_increment, + location_code varchar(200) NOT NULL, + tax_rate_id BIGINT UNSIGNED NOT NULL, + location_type varchar(40) NOT NULL, + PRIMARY KEY (location_id), + KEY tax_rate_id (tax_rate_id), + KEY location_type_code (location_type(10),location_code(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zones ( + zone_id BIGINT UNSIGNED NOT NULL auto_increment, + zone_name varchar(200) NOT NULL, + zone_order BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (zone_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations ( + location_id BIGINT UNSIGNED NOT NULL auto_increment, + zone_id BIGINT UNSIGNED NOT NULL, + location_code varchar(200) NOT NULL, + location_type varchar(40) NOT NULL, + PRIMARY KEY (location_id), + KEY location_id (location_id), + KEY location_type_code (location_type(10),location_code(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods ( + zone_id BIGINT UNSIGNED NOT NULL, + instance_id BIGINT UNSIGNED NOT NULL auto_increment, + method_id varchar(200) NOT NULL, + method_order BIGINT UNSIGNED NOT NULL, + is_enabled tinyint(1) NOT NULL DEFAULT '1', + PRIMARY KEY (instance_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokens ( + token_id BIGINT UNSIGNED NOT NULL auto_increment, + gateway_id varchar(200) NOT NULL, + token text NOT NULL, + user_id BIGINT UNSIGNED NOT NULL DEFAULT '0', + type varchar(200) NOT NULL, + is_default tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (token_id), + KEY user_id (user_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta ( + meta_id BIGINT UNSIGNED NOT NULL auto_increment, + payment_token_id BIGINT UNSIGNED NOT NULL, + meta_key varchar(255) NULL, + meta_value longtext NULL, + PRIMARY KEY (meta_id), + KEY payment_token_id (payment_token_id), + KEY meta_key (meta_key(32)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_log ( + log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp datetime NOT NULL, + level smallint(4) NOT NULL, + source varchar(200) NOT NULL, + message longtext NOT NULL, + context longtext NULL, + PRIMARY KEY (log_id), + KEY level (level) +) $collate; + "; + + /** + * Term meta is only needed for old installs and is now @deprecated by WordPress term meta. + */ + if ( ! function_exists( 'get_term_meta' ) ) { + $tables .= " +CREATE TABLE {$wpdb->prefix}woocommerce_termmeta ( + meta_id BIGINT UNSIGNED NOT NULL auto_increment, + woocommerce_term_id BIGINT UNSIGNED NOT NULL, + meta_key varchar(255) default NULL, + meta_value longtext NULL, + PRIMARY KEY (meta_id), + KEY woocommerce_term_id (woocommerce_term_id), + KEY meta_key (meta_key(32)) +) $collate; + "; + } + + return $tables; + } + + /** + * Create roles and capabilities. + */ + public static function create_roles() { global $wp_roles; - if ( class_exists( 'WP_Roles' ) ) { - if ( ! isset( $wp_roles ) ) { - $wp_roles = new WP_Roles(); - } + if ( ! class_exists( 'WP_Roles' ) ) { + return; } - if ( is_object( $wp_roles ) ) { + if ( ! isset( $wp_roles ) ) { + $wp_roles = new WP_Roles(); + } - // Customer role - add_role( 'customer', __( 'Customer', 'woocommerce' ), array( - 'read' => true, - 'edit_posts' => false, - 'delete_posts' => false - ) ); + // Customer role + add_role( 'customer', __( 'Customer', 'woocommerce' ), array( + 'read' => true, + ) ); - // Shop manager role - add_role( 'shop_manager', __( 'Shop Manager', 'woocommerce' ), array( - 'level_9' => true, - 'level_8' => true, - 'level_7' => true, - 'level_6' => true, - 'level_5' => true, - 'level_4' => true, - 'level_3' => true, - 'level_2' => true, - 'level_1' => true, - 'level_0' => true, - 'read' => true, - 'read_private_pages' => true, - 'read_private_posts' => true, - 'edit_users' => true, - 'edit_posts' => true, - 'edit_pages' => true, - 'edit_published_posts' => true, - 'edit_published_pages' => true, - 'edit_private_pages' => true, - 'edit_private_posts' => true, - 'edit_others_posts' => true, - 'edit_others_pages' => true, - 'publish_posts' => true, - 'publish_pages' => true, - 'delete_posts' => true, - 'delete_pages' => true, - 'delete_private_pages' => true, - 'delete_private_posts' => true, - 'delete_published_pages' => true, - 'delete_published_posts' => true, - 'delete_others_posts' => true, - 'delete_others_pages' => true, - 'manage_categories' => true, - 'manage_links' => true, - 'moderate_comments' => true, - 'unfiltered_html' => true, - 'upload_files' => true, - 'export' => true, - 'import' => true, - 'list_users' => true - ) ); + // Shop manager role + add_role( 'shop_manager', __( 'Shop manager', 'woocommerce' ), array( + 'level_9' => true, + 'level_8' => true, + 'level_7' => true, + 'level_6' => true, + 'level_5' => true, + 'level_4' => true, + 'level_3' => true, + 'level_2' => true, + 'level_1' => true, + 'level_0' => true, + 'read' => true, + 'read_private_pages' => true, + 'read_private_posts' => true, + 'edit_users' => true, + 'edit_posts' => true, + 'edit_pages' => true, + 'edit_published_posts' => true, + 'edit_published_pages' => true, + 'edit_private_pages' => true, + 'edit_private_posts' => true, + 'edit_others_posts' => true, + 'edit_others_pages' => true, + 'publish_posts' => true, + 'publish_pages' => true, + 'delete_posts' => true, + 'delete_pages' => true, + 'delete_private_pages' => true, + 'delete_private_posts' => true, + 'delete_published_pages' => true, + 'delete_published_posts' => true, + 'delete_others_posts' => true, + 'delete_others_pages' => true, + 'manage_categories' => true, + 'manage_links' => true, + 'moderate_comments' => true, + 'unfiltered_html' => true, + 'upload_files' => true, + 'export' => true, + 'import' => true, + 'list_users' => true, + ) ); - $capabilities = $this->get_core_capabilities(); + $capabilities = self::get_core_capabilities(); - foreach ( $capabilities as $cap_group ) { - foreach ( $cap_group as $cap ) { - $wp_roles->add_cap( 'shop_manager', $cap ); - $wp_roles->add_cap( 'administrator', $cap ); - } + foreach ( $capabilities as $cap_group ) { + foreach ( $cap_group as $cap ) { + $wp_roles->add_cap( 'shop_manager', $cap ); + $wp_roles->add_cap( 'administrator', $cap ); } } } /** - * Get capabilities for WooCommerce - these are assigned to admin/shop manager during installation or reset + * Get capabilities for WooCommerce - these are assigned to admin/shop manager during installation or reset. * - * @access public * @return array */ - public function get_core_capabilities() { + private static function get_core_capabilities() { $capabilities = array(); $capabilities['core'] = array( 'manage_woocommerce', - 'view_woocommerce_reports' + 'view_woocommerce_reports', ); - $capability_types = array( 'product', 'shop_order', 'shop_coupon' ); + $capability_types = array( 'product', 'shop_order', 'shop_coupon', 'shop_webhook' ); foreach ( $capability_types as $capability_type ) { @@ -544,7 +756,7 @@ class WC_Install { "manage_{$capability_type}_terms", "edit_{$capability_type}_terms", "delete_{$capability_type}_terms", - "assign_{$capability_type}_terms" + "assign_{$capability_type}_terms", ); } @@ -553,65 +765,65 @@ class WC_Install { /** * woocommerce_remove_roles function. - * - * @access public - * @return void */ - public function remove_roles() { + public static function remove_roles() { global $wp_roles; - if ( class_exists( 'WP_Roles' ) ) { - if ( ! isset( $wp_roles ) ) { - $wp_roles = new WP_Roles(); + if ( ! class_exists( 'WP_Roles' ) ) { + return; + } + + if ( ! isset( $wp_roles ) ) { + $wp_roles = new WP_Roles(); + } + + $capabilities = self::get_core_capabilities(); + + foreach ( $capabilities as $cap_group ) { + foreach ( $cap_group as $cap ) { + $wp_roles->remove_cap( 'shop_manager', $cap ); + $wp_roles->remove_cap( 'administrator', $cap ); } } - if ( is_object( $wp_roles ) ) { - - $capabilities = $this->get_core_capabilities(); - - foreach ( $capabilities as $cap_group ) { - foreach ( $cap_group as $cap ) { - $wp_roles->remove_cap( 'shop_manager', $cap ); - $wp_roles->remove_cap( 'administrator', $cap ); - } - } - - remove_role( 'customer' ); - remove_role( 'shop_manager' ); - } + remove_role( 'customer' ); + remove_role( 'shop_manager' ); } /** - * Create files/directories + * Create files/directories. */ - private function create_files() { + private static function create_files() { // Install files and folders for uploading files and prevent hotlinking - $upload_dir = wp_upload_dir(); + $upload_dir = wp_upload_dir(); + $download_method = get_option( 'woocommerce_file_download_method', 'force' ); $files = array( - array( - 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', - 'file' => '.htaccess', - 'content' => 'deny from all' - ), array( 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', 'file' => 'index.html', - 'content' => '' + 'content' => '', ), array( 'base' => WC_LOG_DIR, 'file' => '.htaccess', - 'content' => 'deny from all' + 'content' => 'deny from all', ), array( 'base' => WC_LOG_DIR, 'file' => 'index.html', - 'content' => '' - ) + 'content' => '', + ), ); + if ( 'redirect' !== $download_method ) { + $files[] = array( + 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', + 'file' => '.htaccess', + 'content' => 'deny from all', + ); + } + foreach ( $files as $file ) { if ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) { if ( $file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'w' ) ) { @@ -622,83 +834,329 @@ class WC_Install { } } - /** - * Create CSS from LESS file - */ - private function create_css_from_less() { - // Recompile LESS styles if they are custom - $colors = get_option( 'woocommerce_frontend_css_colors' ); - - if ( ( ! empty( $colors['primary'] ) && ! empty( $colors['secondary'] ) && ! empty( $colors['highlight'] ) && ! empty( $colors['content_bg'] ) && ! empty( $colors['subtext'] ) ) && ( $colors['primary'] != '#ad74a2' || $colors['secondary'] != '#f7f6f7' || $colors['highlight'] != '#85ad74' || $colors['content_bg'] != '#ffffff' || $colors['subtext'] != '#777777' ) ) { - if ( ! function_exists( 'woocommerce_compile_less_styles' ) ) { - include_once( 'admin/wc-admin-functions.php' ); - } - woocommerce_compile_less_styles(); - } - } - - /** - * Active plugins pre update option filter - * - * @param string $new_value - * @return string - */ - function pre_update_option_active_plugins( $new_value ) { - $old_value = (array) get_option( 'active_plugins' ); - - if ( $new_value !== $old_value && in_array( W3TC_FILE, (array) $new_value ) && in_array( W3TC_FILE, (array) $old_value ) ) { - $this->_config->set( 'notes.plugins_updated', true ); - try { - $this->_config->save(); - } catch( Exception $ex ) {} - } - - return $new_value; - } - /** * Show plugin changes. Code adapted from W3 Total Cache. * - * @return void + * @param array $args */ - function in_plugin_update_message( $args ) { + public static function in_plugin_update_message( $args ) { $transient_name = 'wc_upgrade_notice_' . $args['Version']; if ( false === ( $upgrade_notice = get_transient( $transient_name ) ) ) { - - $response = wp_remote_get( 'https://plugins.svn.wordpress.org/woocommerce/trunk/readme.txt' ); + $response = wp_safe_remote_get( 'https://plugins.svn.wordpress.org/woocommerce/trunk/readme.txt' ); if ( ! is_wp_error( $response ) && ! empty( $response['body'] ) ) { - - // Output Upgrade Notice - $matches = null; - $regexp = '~==\s*Upgrade Notice\s*==\s*=\s*(.*)\s*=(.*)(=\s*' . preg_quote( WC_VERSION ) . '\s*=|$)~Uis'; - $upgrade_notice = ''; - - if ( preg_match( $regexp, $response['body'], $matches ) ) { - $version = trim( $matches[1] ); - $notices = (array) preg_split('~[\r\n]+~', trim( $matches[2] ) ); - - if ( version_compare( WC_VERSION, $version, '<' ) ) { - - $upgrade_notice .= '
    '; - - foreach ( $notices as $index => $line ) { - $upgrade_notice .= wp_kses_post( preg_replace( '~\[([^\]]*)\]\(([^\)]*)\)~', '${1}', $line ) ); - } - - $upgrade_notice .= '
    '; - } - } - + $upgrade_notice = self::parse_update_notice( $response['body'], $args['new_version'] ); set_transient( $transient_name, $upgrade_notice, DAY_IN_SECONDS ); } } - echo wp_kses_post( $upgrade_notice ); + echo apply_filters( 'woocommerce_in_plugin_update_message', wp_kses_post( $upgrade_notice ) ); + } + + /** + * Parse update notice from readme file. + * + * @param string $content + * @param string $new_version + * @return string + */ + private static function parse_update_notice( $content, $new_version ) { + // Output Upgrade Notice. + $matches = null; + $regexp = '~==\s*Upgrade Notice\s*==\s*=\s*(.*)\s*=(.*)(=\s*' . preg_quote( WC_VERSION ) . '\s*=|$)~Uis'; + $upgrade_notice = ''; + + if ( preg_match( $regexp, $content, $matches ) ) { + $notices = (array) preg_split( '~[\r\n]+~', trim( $matches[2] ) ); + + // Convert the full version strings to minor versions. + $notice_version_parts = explode( '.', trim( $matches[1] ) ); + $current_version_parts = explode( '.', WC_VERSION ); + + if ( 3 !== sizeof( $notice_version_parts ) ) { + return; + } + + $notice_version = $notice_version_parts[0] . '.' . $notice_version_parts[1]; + $current_version = $current_version_parts[0] . '.' . $current_version_parts[1]; + + // Check the latest stable version and ignore trunk. + if ( version_compare( $current_version, $notice_version, '<' ) ) { + + $upgrade_notice .= '

    '; + + foreach ( $notices as $index => $line ) { + $upgrade_notice .= preg_replace( '~\[([^\]]*)\]\(([^\)]*)\)~', '${1}', $line ); + } + } + } + + return wp_kses_post( $upgrade_notice ); + } + + /** + * Show action links on the plugin screen. + * + * @param mixed $links Plugin Action links + * @return array + */ + public static function plugin_action_links( $links ) { + $action_links = array( + 'settings' => '' . esc_html__( 'Settings', 'woocommerce' ) . '', + ); + + return array_merge( $action_links, $links ); + } + + /** + * Show row meta on the plugin screen. + * + * @param mixed $links Plugin Row Meta + * @param mixed $file Plugin Base file + * @return array + */ + public static function plugin_row_meta( $links, $file ) { + if ( WC_PLUGIN_BASENAME == $file ) { + $row_meta = array( + 'docs' => '' . esc_html__( 'Docs', 'woocommerce' ) . '', + 'apidocs' => '' . esc_html__( 'API docs', 'woocommerce' ) . '', + 'support' => '' . esc_html__( 'Premium support', 'woocommerce' ) . '', + ); + + return array_merge( $links, $row_meta ); + } + + return (array) $links; + } + + /** + * Uninstall tables when MU blog is deleted. + * @param array $tables + * @return string[] + */ + public static function wpmu_drop_tables( $tables ) { + global $wpdb; + + $tables[] = $wpdb->prefix . 'woocommerce_sessions'; + $tables[] = $wpdb->prefix . 'woocommerce_api_keys'; + $tables[] = $wpdb->prefix . 'woocommerce_attribute_taxonomies'; + $tables[] = $wpdb->prefix . 'woocommerce_downloadable_product_permissions'; + $tables[] = $wpdb->prefix . 'woocommerce_termmeta'; + $tables[] = $wpdb->prefix . 'woocommerce_tax_rates'; + $tables[] = $wpdb->prefix . 'woocommerce_tax_rate_locations'; + $tables[] = $wpdb->prefix . 'woocommerce_order_items'; + $tables[] = $wpdb->prefix . 'woocommerce_order_itemmeta'; + $tables[] = $wpdb->prefix . 'woocommerce_payment_tokens'; + $tables[] = $wpdb->prefix . 'woocommerce_shipping_zones'; + $tables[] = $wpdb->prefix . 'woocommerce_shipping_zone_locations'; + $tables[] = $wpdb->prefix . 'woocommerce_shipping_zone_methods'; + + return $tables; + } + + /** + * Get slug from path + * @param string $key + * @return string + */ + private static function format_plugin_slug( $key ) { + $slug = explode( '/', $key ); + $slug = explode( '.', end( $slug ) ); + return $slug[0]; + } + + /** + * Install a plugin from .org in the background via a cron job (used by + * installer - opt in). + * @param string $plugin_to_install_id + * @param array $plugin_to_install + * @since 2.6.0 + */ + public static function background_installer( $plugin_to_install_id, $plugin_to_install ) { + // Explicitly clear the event. + wp_clear_scheduled_hook( 'woocommerce_plugin_background_installer', func_get_args() ); + + if ( ! empty( $plugin_to_install['repo-slug'] ) ) { + require_once( ABSPATH . 'wp-admin/includes/file.php' ); + require_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); + require_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin; + $upgrader = new WP_Upgrader( $skin ); + $installed_plugins = array_map( array( __CLASS__, 'format_plugin_slug' ), array_keys( get_plugins() ) ); + $plugin_slug = $plugin_to_install['repo-slug']; + $plugin = $plugin_slug . '/' . $plugin_slug . '.php'; + $installed = false; + $activate = false; + + // See if the plugin is installed already + if ( in_array( $plugin_to_install['repo-slug'], $installed_plugins ) ) { + $installed = true; + $activate = ! is_plugin_active( $plugin ); + } + + // Install this thing! + if ( ! $installed ) { + // Suppress feedback + ob_start(); + + try { + $plugin_information = plugins_api( 'plugin_information', array( + 'slug' => $plugin_to_install['repo-slug'], + 'fields' => array( + 'short_description' => false, + 'sections' => false, + 'requires' => false, + 'rating' => false, + 'ratings' => false, + 'downloaded' => false, + 'last_updated' => false, + 'added' => false, + 'tags' => false, + 'homepage' => false, + 'donate_link' => false, + 'author_profile' => false, + 'author' => false, + ), + ) ); + + if ( is_wp_error( $plugin_information ) ) { + throw new Exception( $plugin_information->get_error_message() ); + } + + $package = $plugin_information->download_link; + $download = $upgrader->download_package( $package ); + + if ( is_wp_error( $download ) ) { + throw new Exception( $download->get_error_message() ); + } + + $working_dir = $upgrader->unpack_package( $download, true ); + + if ( is_wp_error( $working_dir ) ) { + throw new Exception( $working_dir->get_error_message() ); + } + + $result = $upgrader->install_package( array( + 'source' => $working_dir, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => false, + 'abort_if_destination_exists' => false, + 'clear_working' => true, + 'hook_extra' => array( + 'type' => 'plugin', + 'action' => 'install', + ), + ) ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + $activate = true; + + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + __( '%1$s could not be installed (%2$s). Please install it manually by clicking here.', 'woocommerce' ), + $plugin_to_install['name'], + $e->getMessage(), + esc_url( admin_url( 'index.php?wc-install-plugin-redirect=' . $plugin_to_install['repo-slug'] ) ) + ) + ); + } + + // Discard feedback + ob_end_clean(); + } + + wp_clean_plugins_cache(); + + // Activate this thing + if ( $activate ) { + try { + $result = activate_plugin( $plugin ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + __( '%1$s was installed but could not be activated. Please activate it manually by clicking here.', 'woocommerce' ), + $plugin_to_install['name'], + admin_url( 'plugins.php' ) + ) + ); + } + } + } + } + + /** + * Install a theme from .org in the background via a cron job (used by installer - opt in). + * + * @param string $theme_slug + * @since 3.1.0 + */ + public static function theme_background_installer( $theme_slug ) { + // Explicitly clear the event. + wp_clear_scheduled_hook( 'woocommerce_theme_background_installer', func_get_args() ); + + if ( ! empty( $theme_slug ) ) { + // Suppress feedback + ob_start(); + + try { + $theme = wp_get_theme( $theme_slug ); + + if ( ! $theme->exists() ) { + require_once( ABSPATH . 'wp-admin/includes/file.php' ); + include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + include_once( ABSPATH . 'wp-admin/includes/theme.php' ); + + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin; + $upgrader = new Theme_Upgrader( $skin ); + $api = themes_api( 'theme_information', array( + 'slug' => $theme_slug, + 'fields' => array( 'sections' => false ), + ) ); + $result = $upgrader->install( $api->download_link ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } elseif ( is_wp_error( $skin->result ) ) { + throw new Exception( $skin->result->get_error_message() ); + } elseif ( is_null( $result ) ) { + throw new Exception( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + } + } + + switch_theme( $theme_slug ); + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $theme_slug . '_install_error', + sprintf( + __( '%1$s could not be installed (%2$s). Please install it manually by clicking here.', 'woocommerce' ), + $theme_slug, + $e->getMessage(), + esc_url( admin_url( 'update.php?action=install-theme&theme=' . $theme_slug . '&_wpnonce=' . wp_create_nonce( 'install-theme_' . $theme_slug ) ) ) + ) + ); + } + + // Discard feedback + ob_end_clean(); + } } } -endif; - -return new WC_Install(); +WC_Install::init(); diff --git a/includes/class-wc-integrations.php b/includes/class-wc-integrations.php index 6822a32d9f3..32bdc72a58b 100644 --- a/includes/class-wc-integrations.php +++ b/includes/class-wc-integrations.php @@ -1,27 +1,33 @@ integrations[ $load_integration->id ] = $load_integration; } - } /** diff --git a/includes/class-wc-language-pack-upgrader.php b/includes/class-wc-language-pack-upgrader.php deleted file mode 100644 index 87986e7bde2..00000000000 --- a/includes/class-wc-language-pack-upgrader.php +++ /dev/null @@ -1,159 +0,0 @@ -repo . WC_VERSION . '/packages/' . $this->get_language() . '.zip'; - } - - /** - * Check for language updates - * - * @param object $data Transient update data - * - * @return object - */ - public function check_for_update( $data ) { - if ( $this->has_available_update() ) { - $data->translations[] = array( - 'type' => 'plugin', - 'slug' => 'woocommerce', - 'language' => $this->get_language(), - 'version' => WC_VERSION, - 'updated' => date( 'Y-m-d H:i:s' ), - 'package' => $this->get_language_package_uri(), - 'autoupdate' => 1 - ); - } - - return $data; - } - - /** - * Check if has available translation update - * - * @return bool - */ - public function has_available_update() { - $version = get_option( 'woocommerce_language_pack_version', '0' ); - - if ( version_compare( $version, WC_VERSION, '<' ) && 'en' !== $this->get_language() ) { - - if ( $this->check_if_language_pack_exists() ) { - $this->configure_woocommerce_upgrade_notice(); - - return true; - } else { - // Updated the woocommerce_language_pack_version to avoid searching translations for this release again - update_option( 'woocommerce_language_pack_version', WC_VERSION ); - } - } - - return false; - } - - /** - * Configure the WooCommerce translation upgrade notice - * - * @return void - */ - public function configure_woocommerce_upgrade_notice() { - $notices = get_option( 'woocommerce_admin_notices', array() ); - if ( false === array_search( 'translation_upgrade', $notices ) ) { - $notices[] = 'translation_upgrade'; - - update_option( 'woocommerce_admin_notices', $notices ); - } - } - - /** - * Check if language pack exists - * - * @return bool - */ - public function check_if_language_pack_exists() { - $response = wp_remote_get( $this->get_language_package_uri(), array( 'sslverify' => false, 'timeout' => 60 ) ); - - if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 ) { - return true; - } else { - return false; - } - } - - /** - * Update the language version in database - * - * @param bool $response Install response (true = success, false = fail) - * @param array $hook_extra Extra arguments passed to hooked filters - * - * @return bool - */ - public function version_update( $response, $hook_extra ) { - if ( $response ) { - if ( - ( isset( $hook_extra['language_update_type'] ) && 'plugin' == $hook_extra['language_update_type'] ) - && ( isset( $hook_extra['language_update']->slug ) && 'woocommerce' == $hook_extra['language_update']->slug ) - ) { - // Update the language pack version - update_option( 'woocommerce_language_pack_version', WC_VERSION ); - - // Remove the translation upgrade notice - $notices = get_option( 'woocommerce_admin_notices', array() ); - $notices = array_diff( $notices, array( 'translation_upgrade' ) ); - update_option( 'woocommerce_admin_notices', $notices ); - } - } - - return $response; - } - -} - -new WC_Language_Pack_Upgrader(); diff --git a/includes/class-wc-legacy-api.php b/includes/class-wc-legacy-api.php new file mode 100644 index 00000000000..fd915c3796a --- /dev/null +++ b/includes/class-wc-legacy-api.php @@ -0,0 +1,271 @@ +query_vars['wc-api-version'] = $_GET['wc-api-version']; + } + + if ( ! empty( $_GET['wc-api-route'] ) ) { + $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; + } + + // REST API request. + if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) { + + define( 'WC_API_REQUEST', true ); + define( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) ); + + // Legacy v1 API request. + if ( 1 === WC_API_REQUEST_VERSION ) { + $this->handle_v1_rest_api_request(); + } elseif ( 2 === WC_API_REQUEST_VERSION ) { + $this->handle_v2_rest_api_request(); + } else { + $this->includes(); + + $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] ); + + // load API resource classes. + $this->register_resources( $this->server ); + + // Fire off the request. + $this->server->serve_request(); + } + + exit; + } + } + + /** + * Include required files for REST API request. + * + * @since 2.1 + * @deprecated 2.6.0 + */ + public function includes() { + + // API server / response handlers. + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-exception.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-json-handler.php' ); + + // Authentication. + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-taxes.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v3/class-wc-api-webhooks.php' ); + + // Allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + } + + /** + * Register available API resources. + * + * @since 2.1 + * @deprecated 2.6.0 + * @param WC_API_Server $server the REST server + */ + public function register_resources( $server ) { + + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Coupons', + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Reports', + 'WC_API_Taxes', + 'WC_API_Webhooks', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $server ); + } + } + + + /** + * Handle legacy v1 REST API requests. + * + * @since 2.2 + * @deprecated 2.6.0 + */ + private function handle_v1_rest_api_request() { + + // Include legacy required files for v1 REST API request. + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v1/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-json-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-xml-handler.php' ); + + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v1/class-wc-api-reports.php' ); + + // Allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + + $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); + + // Register available resources for legacy v1 REST API request. + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Coupons', + 'WC_API_Reports', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $this->server ); + } + + // Fire off the request. + $this->server->serve_request(); + } + + /** + * Handle legacy v2 REST API requests. + * + * @since 2.4 + * @deprecated 2.6.0 + */ + private function handle_v2_rest_api_request() { + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-exception.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-json-handler.php' ); + + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/v2/class-wc-api-webhooks.php' ); + + // allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + + $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); + + // Register available resources for legacy v2 REST API request. + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Coupons', + 'WC_API_Reports', + 'WC_API_Webhooks', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $this->server ); + } + + // Fire off the request. + $this->server->serve_request(); + } +} diff --git a/includes/class-wc-log-levels.php b/includes/class-wc-log-levels.php new file mode 100644 index 00000000000..b29a3069499 --- /dev/null +++ b/includes/class-wc-log-levels.php @@ -0,0 +1,116 @@ + 800, + self::ALERT => 700, + self::CRITICAL => 600, + self::ERROR => 500, + self::WARNING => 400, + self::NOTICE => 300, + self::INFO => 200, + self::DEBUG => 100, + ); + + /** + * Severity integers mapped to level strings. + * + * This is the inverse of $level_severity. + * + * @var array + */ + protected static $severity_to_level = array( + 800 => self::EMERGENCY, + 700 => self::ALERT, + 600 => self::CRITICAL, + 500 => self::ERROR, + 400 => self::WARNING, + 300 => self::NOTICE, + 200 => self::INFO, + 100 => self::DEBUG, + ); + + + /** + * Validate a level string. + * + * @param string $level + * @return bool True if $level is a valid level. + */ + public static function is_valid_level( $level ) { + return array_key_exists( strtolower( $level ), self::$level_to_severity ); + } + + /** + * Translate level string to integer. + * + * @param string $level emergency|alert|critical|error|warning|notice|info|debug + * @return int 100 (debug) - 800 (emergency) or 0 if not recognized + */ + public static function get_level_severity( $level ) { + if ( self::is_valid_level( $level ) ) { + $severity = self::$level_to_severity[ strtolower( $level ) ]; + } else { + $severity = 0; + } + return $severity; + } + + /** + * Translate severity integer to level string. + * + * @param int $severity + * @return bool|string False if not recognized. Otherwise string representation of level. + */ + public static function get_severity_level( $severity ) { + if ( array_key_exists( $severity, self::$severity_to_level ) ) { + return self::$severity_to_level[ $severity ]; + } else { + return false; + } + } + +} diff --git a/includes/class-wc-logger.php b/includes/class-wc-logger.php index 50a6844566f..f93bacafb6f 100644 --- a/includes/class-wc-logger.php +++ b/includes/class-wc-logger.php @@ -1,92 +1,273 @@ _handles = array(); - } - - - /** - * Destructor. - * - * @access public - * @return void - */ - public function __destruct() { - foreach ( $this->_handles as $handle ) { - @fclose( escapeshellarg( $handle ) ); + public function __construct( $handlers = null, $threshold = null ) { + if ( null === $handlers ) { + $handlers = apply_filters( 'woocommerce_register_log_handlers', array() ); } + + $register_handlers = array(); + + if ( ! empty( $handlers ) && is_array( $handlers ) ) { + foreach ( $handlers as $handler ) { + $implements = class_implements( $handler ); + if ( is_object( $handler ) && is_array( $implements ) && in_array( 'WC_Log_Handler_Interface', $implements ) ) { + $register_handlers[] = $handler; + } else { + wc_doing_it_wrong( + __METHOD__, + sprintf( + __( 'The provided handler %s does not implement WC_Log_Handler_Interface.', 'woocommerce' ), + esc_html( is_object( $handler ) ? get_class( $handler ) : $handler ) + ), + '3.0' + ); + } + } + } + + if ( null !== $threshold ) { + $threshold = WC_Log_Levels::get_level_severity( $threshold ); + } elseif ( defined( 'WC_LOG_THRESHOLD' ) && WC_Log_Levels::is_valid_level( WC_LOG_THRESHOLD ) ) { + $threshold = WC_Log_Levels::get_level_severity( WC_LOG_THRESHOLD ); + } else { + $threshold = null; + } + + $this->handlers = $register_handlers; + $this->threshold = $threshold; } - /** - * Open log file for writing. + * Determine whether to handle or ignore log. * - * @access private - * @param mixed $handle - * @return bool success + * @param string $level emergency|alert|critical|error|warning|notice|info|debug + * @return bool True if the log should be handled. */ - private function open( $handle ) { - if ( isset( $this->_handles[ $handle ] ) ) { + protected function should_handle( $level ) { + if ( null === $this->threshold ) { return true; } - - if ( $this->_handles[ $handle ] = @fopen( wc_get_log_file_path( $handle ), 'a' ) ) { - return true; - } - - return false; + return $this->threshold <= WC_Log_Levels::get_level_severity( $level ); } - /** - * Add a log entry to chosen file. + * Add a log entry. * - * @access public - * @param mixed $handle - * @param mixed $message - * @return void + * This is not the preferred method for adding log messages. Please use log() or any one of + * the level methods (debug(), info(), etc.). This method may be deprecated in the future. + * + * @param string $handle + * @param string $message + * @param string $level + * + * @return bool */ - public function add( $handle, $message ) { - if ( $this->open( $handle ) && is_resource( $this->_handles[ $handle ] ) ) { - $time = date_i18n( 'm-d-Y @ H:i:s -' ); // Grab Time - @fwrite( $this->_handles[ $handle ], $time . " " . $message . "\n" ); + public function add( $handle, $message, $level = WC_Log_Levels::NOTICE ) { + $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); + $this->log( $level, $message, array( 'source' => $handle, '_legacy' => true ) ); + wc_do_deprecated_action( 'woocommerce_log_add', array( $handle, $message ), '3.0', 'This action has been deprecated with no alternative.' ); + return true; + } + + /** + * Add a log entry. + * + * @param string $level One of the following: + * 'emergency': System is unusable. + * 'alert': Action must be taken immediately. + * 'critical': Critical conditions. + * 'error': Error conditions. + * 'warning': Warning conditions. + * 'notice': Normal but significant condition. + * 'info': Informational messages. + * 'debug': Debug-level messages. + * @param string $message Log message. + * @param array $context Optional. Additional information for log handlers. + */ + public function log( $level, $message, $context = array() ) { + if ( ! WC_Log_Levels::is_valid_level( $level ) ) { + wc_doing_it_wrong( __METHOD__, sprintf( __( 'WC_Logger::log was called with an invalid level "%s".', 'woocommerce' ), $level ), '3.0' ); + } + + if ( $this->should_handle( $level ) ) { + $timestamp = current_time( 'timestamp' ); + $message = apply_filters( 'woocommerce_logger_log_message', $message, $level, $context ); + + foreach ( $this->handlers as $handler ) { + $handler->handle( $timestamp, $level, $message, $context ); + } } } + /** + * Adds an emergency level message. + * + * System is unusable. + * + * @see WC_Logger::log + * + * @param string $message + * @param array $context + */ + public function emergency( $message, $context = array() ) { + $this->log( WC_Log_Levels::EMERGENCY, $message, $context ); + } + + /** + * Adds an alert level message. + * + * Action must be taken immediately. + * Example: Entire website down, database unavailable, etc. + * + * @see WC_Logger::log + * + * @param string $message + * @param array $context + */ + public function alert( $message, $context = array() ) { + $this->log( WC_Log_Levels::ALERT, $message, $context ); + } + + /** + * Adds a critical level message. + * + * Critical conditions. + * Example: Application component unavailable, unexpected exception. + * + * @see WC_Logger::log + * + * @param string $message + * @param array $context + */ + public function critical( $message, $context = array() ) { + $this->log( WC_Log_Levels::CRITICAL, $message, $context ); + } + + /** + * Adds an error level message. + * + * Runtime errors that do not require immediate action but should typically be logged + * and monitored. + * + * @see WC_Logger::log + * + * @param string $message + * @param array $context + */ + public function error( $message, $context = array() ) { + $this->log( WC_Log_Levels::ERROR, $message, $context ); + } + + /** + * Adds a warning level message. + * + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not + * necessarily wrong. + * + * @see WC_Logger::log + * + * @param string $message + * @param array $context + */ + public function warning( $message, $context = array() ) { + $this->log( WC_Log_Levels::WARNING, $message, $context ); + } + + /** + * Adds a notice level message. + * + * Normal but significant events. + * + * @see WC_Logger::log + * + * @param string $message + * @param array $context + */ + public function notice( $message, $context = array() ) { + $this->log( WC_Log_Levels::NOTICE, $message, $context ); + } + + /** + * Adds a info level message. + * + * Interesting events. + * Example: User logs in, SQL logs. + * + * @see WC_Logger::log + * + * @param string $message + * @param array $context + */ + public function info( $message, $context = array() ) { + $this->log( WC_Log_Levels::INFO, $message, $context ); + } + + /** + * Adds a debug level message. + * + * Detailed debug information. + * + * @see WC_Logger::log + * + * @param string $message + * @param array $context + */ + public function debug( $message, $context = array() ) { + $this->log( WC_Log_Levels::DEBUG, $message, $context ); + } /** * Clear entries from chosen file. * - * @access public - * @param mixed $handle - * @return void + * @deprecated 3.0.0 + * + * @param string $handle + * + * @return bool */ public function clear( $handle ) { - if ( $this->open( $handle ) && is_resource( $this->_handles[ $handle ] ) ) { - @ftruncate( $this->_handles[ $handle ], 0 ); - } + wc_deprecated_function( 'WC_Logger::clear', '3.0', 'WC_Log_Handler_File::clear' ); + $handler = new WC_Log_Handler_File(); + return $handler->clear( $handle ); } - } diff --git a/includes/class-wc-order-factory.php b/includes/class-wc-order-factory.php index c4d28f74e3b..dfd21ffc03e 100644 --- a/includes/class-wc-order-factory.php +++ b/includes/class-wc-order-factory.php @@ -1,57 +1,129 @@ ID ); - $post_type = $the_order->post_type; } - if ( 'shop_order' == $post_type ) { - $classname = 'WC_Order'; - $order_type = 'simple'; + $order_type = WC_Data_Store::load( 'order' )->get_order_type( $order_id ); + if ( $order_type_data = wc_get_order_type( $order_type ) ) { + $classname = $order_type_data['class_name']; } else { $classname = false; - $order_type = false; } // Filter classname so that the class can be overridden if extended. - $classname = apply_filters( 'woocommerce_order_class', $classname, $order_type, $post_type, $order_id ); + $classname = apply_filters( 'woocommerce_order_class', $classname, $order_type, $order_id ); - if ( ! class_exists( $classname ) ) - $classname = 'WC_Order'; + if ( ! class_exists( $classname ) ) { + return false; + } - return new $classname( $the_order, $args ); + try { + return new $classname( $order_id ); + } catch ( Exception $e ) { + return false; + } } -} \ No newline at end of file + + /** + * Get order item. + * @param int + * @return WC_Order_Item|false if not found + */ + public static function get_order_item( $item_id = 0 ) { + if ( is_numeric( $item_id ) ) { + $item_type = WC_Data_Store::load( 'order-item' )->get_order_item_type( $item_id ); + $id = $item_id; + } elseif ( $item_id instanceof WC_Order_Item ) { + $item_type = $item_id->get_type(); + $id = $item_id->get_id(); + } elseif ( is_object( $item_id ) && ! empty( $item_id->order_item_type ) ) { + $id = $item_id->order_item_id; + $item_type = $item_id->order_item_type; + } else { + $item_type = false; + $id = false; + } + + if ( $id && $item_type ) { + $classname = false; + switch ( $item_type ) { + case 'line_item' : + case 'product' : + $classname = 'WC_Order_Item_Product'; + break; + case 'coupon' : + $classname = 'WC_Order_Item_Coupon'; + break; + case 'fee' : + $classname = 'WC_Order_Item_Fee'; + break; + case 'shipping' : + $classname = 'WC_Order_Item_Shipping'; + break; + case 'tax' : + $classname = 'WC_Order_Item_Tax'; + break; + } + + $classname = apply_filters( 'woocommerce_get_order_item_classname', $classname, $item_type, $id ); + + if ( $classname && class_exists( $classname ) ) { + try { + return new $classname( $id ); + } catch ( Exception $e ) { + return false; + } + } + } + return false; + } + + /** + * Get the order ID depending on what was passed. + * + * @since 3.0.0 + * @param mixed $order + * @return int|bool false on failure + */ + public static function get_order_id( $order ) { + global $post; + + if ( false === $order && is_a( $post, 'WP_Post' ) && 'shop_order' === get_post_type( $post ) ) { + return $post->ID; + } elseif ( is_numeric( $order ) ) { + return $order; + } elseif ( $order instanceof WC_Abstract_Order ) { + return $order->get_id(); + } elseif ( ! empty( $order->ID ) ) { + return $order->ID; + } else { + return false; + } + } +} diff --git a/includes/class-wc-order-item-coupon.php b/includes/class-wc-order-item-coupon.php new file mode 100644 index 00000000000..b0b3ad04474 --- /dev/null +++ b/includes/class-wc-order-item-coupon.php @@ -0,0 +1,179 @@ + '', + 'discount' => 0, + 'discount_tax' => 0, + ); + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set order item name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_name( $value ) { + return $this->set_code( $value ); + } + + /** + * Set code. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_code( $value ) { + $this->set_prop( 'code', wc_clean( $value ) ); + } + + /** + * Set discount amount. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_discount( $value ) { + $this->set_prop( 'discount', wc_format_decimal( $value ) ); + } + + /** + * Set discounted tax amount. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_discount_tax( $value ) { + $this->set_prop( 'discount_tax', wc_format_decimal( $value ) ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'coupon'; + } + + /** + * Get order item name. + * + * @param string $context + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_code( $context ); + } + + /** + * Get coupon code. + * + * @param string $context + * @return string + */ + public function get_code( $context = 'view' ) { + return $this->get_prop( 'code', $context ); + } + + /** + * Get discount amount. + * + * @param string $context + * @return string + */ + public function get_discount( $context = 'view' ) { + return $this->get_prop( 'discount', $context ); + } + + /** + * Get discounted tax amount. + * + * @param string $context + * + * @return string + */ + public function get_discount_tax( $context = 'view' ) { + return $this->get_prop( 'discount_tax', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compat with legacy arrays. + | + */ + + /** + * offsetGet for ArrayAccess/Backwards compatibility. + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + if ( 'discount_amount' === $offset ) { + $offset = 'discount'; + } elseif ( 'discount_amount_tax' === $offset ) { + $offset = 'discount_tax'; + } + return parent::offsetGet( $offset ); + } + + /** + * offsetSet for ArrayAccess/Backwards compatibility. + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + if ( 'discount_amount' === $offset ) { + $offset = 'discount'; + } elseif ( 'discount_amount_tax' === $offset ) { + $offset = 'discount_tax'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * offsetExists for ArrayAccess + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'discount_amount', 'discount_amount_tax' ) ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/includes/class-wc-order-item-fee.php b/includes/class-wc-order-item-fee.php new file mode 100644 index 00000000000..f811684a8f1 --- /dev/null +++ b/includes/class-wc-order-item-fee.php @@ -0,0 +1,240 @@ + '', + 'tax_status' => 'taxable', + 'total' => '', + 'total_tax' => '', + 'taxes' => array( + 'total' => array(), + ), + ); + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set tax class. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_tax_class( $value ) { + if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs() ) ) { + $this->error( 'order_item_fee_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); + } + $this->set_prop( 'tax_class', $value ); + } + + /** + * Set tax_status. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_tax_status( $value ) { + if ( in_array( $value, array( 'taxable', 'none' ) ) ) { + $this->set_prop( 'tax_status', $value ); + } else { + $this->set_prop( 'tax_status', 'taxable' ); + } + } + + /** + * Set total. + * + * @param string $amount Fee amount (do not enter negative amounts). + * @throws WC_Data_Exception + */ + public function set_total( $amount ) { + $this->set_prop( 'total', wc_format_decimal( $amount ) ); + } + + /** + * Set total tax. + * + * @param string $amount + * @throws WC_Data_Exception + */ + public function set_total_tax( $amount ) { + $this->set_prop( 'total_tax', wc_format_decimal( $amount ) ); + } + + /** + * Set taxes. + * + * This is an array of tax ID keys with total amount values. + * @param array $raw_tax_data + * @throws WC_Data_Exception + */ + public function set_taxes( $raw_tax_data ) { + $raw_tax_data = maybe_unserialize( $raw_tax_data ); + $tax_data = array( + 'total' => array(), + ); + if ( ! empty( $raw_tax_data['total'] ) ) { + $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); + } + $this->set_prop( 'taxes', $tax_data ); + $this->set_total_tax( array_sum( $tax_data['total'] ) ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item name. + * + * @param string $context + * @return string + */ + public function get_name( $context = 'view' ) { + $name = $this->get_prop( 'name', $context ); + if ( 'view' === $context ) { + return $name ? $name : __( 'Fee', 'woocommerce' ); + } else { + return $name; + } + } + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'fee'; + } + + /** + * Get tax class. + * + * @param string $context + * @return string + */ + public function get_tax_class( $context = 'view' ) { + return $this->get_prop( 'tax_class', $context ); + } + + /** + * Get tax status. + * + * @param string $context + * @return string + */ + public function get_tax_status( $context = 'view' ) { + return $this->get_prop( 'tax_status', $context ); + } + + /** + * Get total fee. + * + * @param string $context + * @return string + */ + public function get_total( $context = 'view' ) { + return $this->get_prop( 'total', $context ); + } + + /** + * Get total tax. + * + * @param string $context + * @return string + */ + public function get_total_tax( $context = 'view' ) { + return $this->get_prop( 'total_tax', $context ); + } + + /** + * Get fee taxes. + * + * @param string $context + * @return array + */ + public function get_taxes( $context = 'view' ) { + return $this->get_prop( 'taxes', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compat with legacy arrays. + | + */ + + /** + * offsetGet for ArrayAccess/Backwards compatibility. + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + if ( 'line_total' === $offset ) { + $offset = 'total'; + } elseif ( 'line_tax' === $offset ) { + $offset = 'total_tax'; + } elseif ( 'line_tax_data' === $offset ) { + $offset = 'taxes'; + } + return parent::offsetGet( $offset ); + } + + /** + * offsetSet for ArrayAccess/Backwards compatibility. + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + if ( 'line_total' === $offset ) { + $offset = 'total'; + } elseif ( 'line_tax' === $offset ) { + $offset = 'total_tax'; + } elseif ( 'line_tax_data' === $offset ) { + $offset = 'taxes'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * offsetExists for ArrayAccess + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'line_total', 'line_tax', 'line_tax_data' ) ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/includes/class-wc-order-item-meta.php b/includes/class-wc-order-item-meta.php index 1c1fff94b11..72f5b693f5a 100644 --- a/includes/class-wc-order-item-meta.php +++ b/includes/class-wc-order-item-meta.php @@ -1,108 +1,199 @@ meta = $item_meta; + public function __construct( $item = array(), $product = null ) { + wc_deprecated_function( 'WC_Order_Item_Meta::__construct', '3.1', 'WC_Order_Item_Product' ); + + // Backwards (pre 2.4) compat + if ( ! isset( $item['item_meta'] ) ) { + $this->legacy = true; + $this->meta = array_filter( (array) $item ); + return; + } + $this->item = $item; + $this->meta = array_filter( (array) $item['item_meta'] ); $this->product = $product; } /** - * Display meta in a formatted list + * Display meta in a formatted list. * - * @access public * @param bool $flat (default: false) * @param bool $return (default: false) * @param string $hideprefix (default: _) - * @return string + * @param string $delimiter Delimiter used to separate items when $flat is true + * @return string|void */ - public function display( $flat = false, $return = false, $hideprefix = '_' ) { - - if ( ! empty( $this->meta ) ) { + public function display( $flat = false, $return = false, $hideprefix = '_', $delimiter = ", \n" ) { + $output = ''; + $formatted_meta = $this->get_formatted( $hideprefix ); + if ( ! empty( $formatted_meta ) ) { $meta_list = array(); - foreach ( $this->meta as $meta_key => $meta_values ) { - - if ( empty( $meta_values ) || ( ! empty( $hideprefix ) && substr( $meta_key, 0, 1 ) == $hideprefix ) ) { - continue; - } - - foreach( $meta_values as $meta_value ) { - - // Skip serialised meta - if ( is_serialized( $meta_value ) ) { - continue; - } - - $attribute_key = urldecode( str_replace( 'attribute_', '', $meta_key ) ); - - // If this is a term slug, get the term's nice name - if ( taxonomy_exists( $attribute_key ) ) { - $term = get_term_by( 'slug', $meta_value, $attribute_key ); - - if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { - $meta_value = $term->name; - } - - // If we have a product, and its not a term, try to find its non-sanitized name - } elseif ( $this->product ) { - $product_attributes = $this->product->get_attributes(); - - if ( isset( $product_attributes[ $attribute_key ] ) ) { - $meta_key = wc_attribute_label( $product_attributes[ $attribute_key ]['name'] ); - } - } - - if ( $flat ) { - $meta_list[] = wp_kses_post( wc_attribute_label( $attribute_key ) . ': ' . apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value ) ); - } else { - $meta_list[] = ' -

    ' . wp_kses_post( wc_attribute_label( $attribute_key ) ) . ':
    -
    ' . wp_kses_post( wpautop( apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value ) ) ) . '
    - '; - } + foreach ( $formatted_meta as $meta ) { + if ( $flat ) { + $meta_list[] = wp_kses_post( $meta['label'] . ': ' . $meta['value'] ); + } else { + $meta_list[] = ' +
    ' . wp_kses_post( $meta['label'] ) . ':
    +
    ' . wp_kses_post( wpautop( make_clickable( $meta['value'] ) ) ) . '
    + '; } } - if ( ! sizeof( $meta_list ) ) - return ''; - - $output = $flat ? '' : '
    '; - - if ( $flat ) - $output .= implode( ", \n", $meta_list ); - else - $output .= implode( '', $meta_list ); - - if ( ! $flat ) - $output .= '
    '; - - if ( $return ) - return $output; - else - echo $output; + if ( ! empty( $meta_list ) ) { + if ( $flat ) { + $output .= implode( $delimiter, $meta_list ); + } else { + $output .= '
    ' . implode( '', $meta_list ) . '
    '; + } + } } - return ''; + $output = apply_filters( 'woocommerce_order_items_meta_display', $output, $this ); + + if ( $return ) { + return $output; + } else { + echo $output; + } } -} \ No newline at end of file + + /** + * Return an array of formatted item meta in format e.g. + * + * array( + * 'pa_size' => array( + * 'label' => 'Size', + * 'value' => 'Medium', + * ) + * ) + * + * @since 2.4 + * @param string $hideprefix exclude meta when key is prefixed with this, defaults to `_` + * @return array + */ + public function get_formatted( $hideprefix = '_' ) { + if ( $this->legacy ) { + return $this->get_formatted_legacy( $hideprefix ); + } + + $formatted_meta = array(); + + if ( ! empty( $this->item['item_meta_array'] ) ) { + foreach ( $this->item['item_meta_array'] as $meta_id => $meta ) { + if ( "" === $meta->value || is_serialized( $meta->value ) || ( ! empty( $hideprefix ) && substr( $meta->key, 0, 1 ) === $hideprefix ) ) { + continue; + } + + $attribute_key = urldecode( str_replace( 'attribute_', '', $meta->key ) ); + $meta_value = $meta->value; + + // If this is a term slug, get the term's nice name + if ( taxonomy_exists( $attribute_key ) ) { + $term = get_term_by( 'slug', $meta_value, $attribute_key ); + + if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { + $meta_value = $term->name; + } + } + + $formatted_meta[ $meta_id ] = array( + 'key' => $meta->key, + 'label' => wc_attribute_label( $attribute_key, $this->product ), + 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $meta, $this->item ), + ); + } + } + + return apply_filters( 'woocommerce_order_items_meta_get_formatted', $formatted_meta, $this ); + } + + /** + * Return an array of formatted item meta in format e.g. + * Handles @deprecated args. + * + * @param string $hideprefix + * + * @return array + */ + public function get_formatted_legacy( $hideprefix = '_' ) { + if ( ! is_ajax() ) { + wc_deprecated_argument( 'WC_Order_Item_Meta::get_formatted', '2.4', 'Item Meta Data is being called with legacy arguments' ); + } + + $formatted_meta = array(); + + foreach ( $this->meta as $meta_key => $meta_values ) { + if ( empty( $meta_values ) || ( ! empty( $hideprefix ) && substr( $meta_key, 0, 1 ) == $hideprefix ) ) { + continue; + } + foreach ( (array) $meta_values as $meta_value ) { + // Skip serialised meta + if ( is_serialized( $meta_value ) ) { + continue; + } + + $attribute_key = urldecode( str_replace( 'attribute_', '', $meta_key ) ); + + // If this is a term slug, get the term's nice name + if ( taxonomy_exists( $attribute_key ) ) { + $term = get_term_by( 'slug', $meta_value, $attribute_key ); + if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { + $meta_value = $term->name; + } + } + + // Unique key required + $formatted_meta_key = $meta_key; + $loop = 0; + while ( isset( $formatted_meta[ $formatted_meta_key ] ) ) { + $loop ++; + $formatted_meta_key = $meta_key . '-' . $loop; + } + + $formatted_meta[ $formatted_meta_key ] = array( + 'key' => $meta_key, + 'label' => wc_attribute_label( $attribute_key, $this->product ), + 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value ), + ); + } + } + + return $formatted_meta; + } +} diff --git a/includes/class-wc-order-item-product.php b/includes/class-wc-order-item-product.php new file mode 100644 index 00000000000..9ea3bb12757 --- /dev/null +++ b/includes/class-wc-order-item-product.php @@ -0,0 +1,462 @@ + 0, + 'variation_id' => 0, + 'quantity' => 1, + 'tax_class' => '', + 'subtotal' => 0, + 'subtotal_tax' => 0, + 'total' => 0, + 'total_tax' => 0, + 'taxes' => array( + 'subtotal' => array(), + 'total' => array(), + ), + ); + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set quantity. + * + * @param int $value + * @throws WC_Data_Exception + */ + public function set_quantity( $value ) { + $this->set_prop( 'quantity', wc_stock_amount( $value ) ); + } + + /** + * Set tax class. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_tax_class( $value ) { + if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs() ) ) { + $this->error( 'order_item_product_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); + } + $this->set_prop( 'tax_class', $value ); + } + + /** + * Set Product ID + * + * @param int $value + * @throws WC_Data_Exception + */ + public function set_product_id( $value ) { + if ( $value > 0 && 'product' !== get_post_type( absint( $value ) ) ) { + $this->error( 'order_item_product_invalid_product_id', __( 'Invalid product ID', 'woocommerce' ) ); + } + $this->set_prop( 'product_id', absint( $value ) ); + } + + /** + * Set variation ID. + * + * @param int $value + * @throws WC_Data_Exception + */ + public function set_variation_id( $value ) { + if ( $value > 0 && 'product_variation' !== get_post_type( $value ) ) { + $this->error( 'order_item_product_invalid_variation_id', __( 'Invalid variation ID', 'woocommerce' ) ); + } + $this->set_prop( 'variation_id', absint( $value ) ); + } + + /** + * Line subtotal (before discounts). + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_subtotal( $value ) { + $this->set_prop( 'subtotal', floatval( wc_format_decimal( $value ) ) ); + } + + /** + * Line total (after discounts). + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_total( $value ) { + $this->set_prop( 'total', floatval( wc_format_decimal( $value ) ) ); + + // Subtotal cannot be less than total + if ( '' === $this->get_subtotal() || $this->get_subtotal() < $this->get_total() ) { + $this->set_subtotal( $value ); + } + } + + /** + * Line subtotal tax (before discounts). + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_subtotal_tax( $value ) { + $this->set_prop( 'subtotal_tax', wc_format_decimal( $value ) ); + } + + /** + * Line total tax (after discounts). + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_total_tax( $value ) { + $this->set_prop( 'total_tax', wc_format_decimal( $value ) ); + } + + /** + * Set line taxes and totals for passed in taxes. + * + * @param array $raw_tax_data + * @throws WC_Data_Exception + */ + public function set_taxes( $raw_tax_data ) { + $raw_tax_data = maybe_unserialize( $raw_tax_data ); + $tax_data = array( + 'total' => array(), + 'subtotal' => array(), + ); + if ( ! empty( $raw_tax_data['total'] ) && ! empty( $raw_tax_data['subtotal'] ) ) { + $tax_data['subtotal'] = array_map( 'wc_format_decimal', $raw_tax_data['subtotal'] ); + $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); + + // Subtotal cannot be less than total! + if ( array_sum( $tax_data['subtotal'] ) < array_sum( $tax_data['total'] ) ) { + $tax_data['subtotal'] = $tax_data['total']; + } + } + $this->set_prop( 'taxes', $tax_data ); + $this->set_total_tax( array_sum( $tax_data['total'] ) ); + $this->set_subtotal_tax( array_sum( $tax_data['subtotal'] ) ); + } + + /** + * Set variation data (stored as meta data - write only). + * + * @param array $data Key/Value pairs + */ + public function set_variation( $data ) { + foreach ( $data as $key => $value ) { + $this->add_meta_data( str_replace( 'attribute_', '', $key ), $value, true ); + } + } + + /** + * Set properties based on passed in product object. + * + * @param WC_Product $product + * @throws WC_Data_Exception + */ + public function set_product( $product ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $this->error( 'order_item_product_invalid_product', __( 'Invalid product', 'woocommerce' ) ); + } + if ( $product->is_type( 'variation' ) ) { + $this->set_product_id( $product->get_parent_id() ); + $this->set_variation_id( $product->get_id() ); + $this->set_variation( is_callable( array( $product, 'get_variation_attributes' ) ) ? $product->get_variation_attributes() : array() ); + } else { + $this->set_product_id( $product->get_id() ); + } + $this->set_name( $product->get_name() ); + $this->set_tax_class( $product->get_tax_class() ); + } + + /** + * Set meta data for backordered products. + */ + public function set_backorder_meta() { + $product = $this->get_product(); + if ( $product && $product->backorders_require_notification() && $product->is_on_backorder( $this->get_quantity() ) ) { + $this->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ) ), $this->get_quantity() - max( 0, $product->get_stock_quantity() ), true ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'line_item'; + } + + /** + * Get product ID. + * + * @param string $context + * @return int + */ + public function get_product_id( $context = 'view' ) { + return $this->get_prop( 'product_id', $context ); + } + + /** + * Get variation ID. + * + * @param string $context + * @return int + */ + public function get_variation_id( $context = 'view' ) { + return $this->get_prop( 'variation_id', $context ); + } + + /** + * Get quantity. + * + * @param string $context + * @return int + */ + public function get_quantity( $context = 'view' ) { + return $this->get_prop( 'quantity', $context ); + } + + /** + * Get tax class. + * + * @param string $context + * @return string + */ + public function get_tax_class( $context = 'view' ) { + return $this->get_prop( 'tax_class', $context ); + } + + /** + * Get subtotal. + * + * @param string $context + * @return string + */ + public function get_subtotal( $context = 'view' ) { + return $this->get_prop( 'subtotal', $context ); + } + + /** + * Get subtotal tax. + * + * @param string $context + * @return string + */ + public function get_subtotal_tax( $context = 'view' ) { + return $this->get_prop( 'subtotal_tax', $context ); + } + + /** + * Get total. + * + * @param string $context + * @return string + */ + public function get_total( $context = 'view' ) { + return $this->get_prop( 'total', $context ); + } + + /** + * Get total tax. + * + * @param string $context + * @return string + */ + public function get_total_tax( $context = 'view' ) { + return $this->get_prop( 'total_tax', $context ); + } + + /** + * Get taxes. + * + * @param string $context + * @return array + */ + public function get_taxes( $context = 'view' ) { + return $this->get_prop( 'taxes', $context ); + } + + /** + * Get the associated product. + * + * @return WC_Product|bool + */ + public function get_product() { + if ( $this->get_variation_id() ) { + $product = wc_get_product( $this->get_variation_id() ); + } else { + $product = wc_get_product( $this->get_product_id() ); + } + + // Backwards compatible filter from WC_Order::get_product_from_item() + if ( has_filter( 'woocommerce_get_product_from_item' ) ) { + $product = apply_filters( 'woocommerce_get_product_from_item', $product, $this, $this->get_order() ); + } + + return apply_filters( 'woocommerce_order_item_product', $product, $this ); + } + + /** + * Get the Download URL. + * + * @param int $download_id + * @return string + */ + public function get_item_download_url( $download_id ) { + $order = $this->get_order(); + + return $order ? add_query_arg( array( + 'download_file' => $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(), + 'order' => $order->get_order_key(), + 'email' => urlencode( $order->get_billing_email() ), + 'key' => $download_id, + ), trailingslashit( home_url() ) ) : ''; + } + + /** + * Get any associated downloadable files. + * + * @return array + */ + public function get_item_downloads() { + $files = array(); + $product = $this->get_product(); + $order = $this->get_order(); + $product_id = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(); + + if ( $product && $order && $product->is_downloadable() && $order->is_download_permitted() ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + $customer_downloads = $data_store->get_downloads( array( + 'user_email' => $order->get_billing_email(), + 'order_id' => $order->get_id(), + 'product_id' => $product_id, + ) ); + foreach ( $customer_downloads as $customer_download ) { + $download_id = $customer_download->get_download_id(); + + if ( $product->has_file( $download_id ) ) { + $file = $product->get_file( $download_id ); + $files[ $download_id ] = $file->get_data(); + $files[ $download_id ]['download_url'] = add_query_arg( array( + 'download_file' => $product_id, + 'order' => $order->get_order_key(), + 'email' => urlencode( $order->get_billing_email() ), + 'key' => $download_id, + ), trailingslashit( home_url() ) ); + } + } + } + + return apply_filters( 'woocommerce_get_item_downloads', $files, $this, $order ); + } + + /** + * Get tax status. + * @return string + */ + public function get_tax_status() { + $product = $this->get_product(); + return $product ? $product->get_tax_status() : 'taxable'; + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compat with legacy arrays. + | + */ + + /** + * offsetGet for ArrayAccess/Backwards compatibility. + * + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + if ( 'line_subtotal' === $offset ) { + $offset = 'subtotal'; + } elseif ( 'line_subtotal_tax' === $offset ) { + $offset = 'subtotal_tax'; + } elseif ( 'line_total' === $offset ) { + $offset = 'total'; + } elseif ( 'line_tax' === $offset ) { + $offset = 'total_tax'; + } elseif ( 'line_tax_data' === $offset ) { + $offset = 'taxes'; + } elseif ( 'qty' === $offset ) { + $offset = 'quantity'; + } + return parent::offsetGet( $offset ); + } + + /** + * offsetSet for ArrayAccess/Backwards compatibility. + * + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + if ( 'line_subtotal' === $offset ) { + $offset = 'subtotal'; + } elseif ( 'line_subtotal_tax' === $offset ) { + $offset = 'subtotal_tax'; + } elseif ( 'line_total' === $offset ) { + $offset = 'total'; + } elseif ( 'line_tax' === $offset ) { + $offset = 'total_tax'; + } elseif ( 'line_tax_data' === $offset ) { + $offset = 'taxes'; + } elseif ( 'qty' === $offset ) { + $offset = 'quantity'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * offsetExists for ArrayAccess + * + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'line_subtotal', 'line_subtotal_tax', 'line_total', 'line_tax', 'line_tax_data', 'item_meta_array', 'item_meta', 'qty' ) ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/includes/class-wc-order-item-shipping.php b/includes/class-wc-order-item-shipping.php new file mode 100644 index 00000000000..df20fae8400 --- /dev/null +++ b/includes/class-wc-order-item-shipping.php @@ -0,0 +1,249 @@ + '', + 'method_id' => '', + 'total' => 0, + 'total_tax' => 0, + 'taxes' => array( + 'total' => array(), + ), + ); + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set order item name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_name( $value ) { + $this->set_method_title( $value ); + } + + /** + * Set method title. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_method_title( $value ) { + $this->set_prop( 'name', wc_clean( $value ) ); + $this->set_prop( 'method_title', wc_clean( $value ) ); + } + + /** + * Set shipping method id. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_method_id( $value ) { + $this->set_prop( 'method_id', wc_clean( $value ) ); + } + + /** + * Set total. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_total( $value ) { + $this->set_prop( 'total', wc_format_decimal( $value ) ); + } + + /** + * Set total tax. + * + * @param string $value + * @throws WC_Data_Exception + */ + protected function set_total_tax( $value ) { + $this->set_prop( 'total_tax', wc_format_decimal( $value ) ); + } + + /** + * Set taxes. + * + * This is an array of tax ID keys with total amount values. + * @param array $raw_tax_data + * @throws WC_Data_Exception + */ + public function set_taxes( $raw_tax_data ) { + $raw_tax_data = maybe_unserialize( $raw_tax_data ); + $tax_data = array( + 'total' => array(), + ); + if ( isset( $raw_tax_data['total'] ) ) { + $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); + } elseif ( ! empty( $raw_tax_data ) && is_array( $raw_tax_data ) ) { + // Older versions just used an array. + $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data ); + } + $this->set_prop( 'taxes', $tax_data ); + $this->set_total_tax( array_sum( $tax_data['total'] ) ); + } + + /** + * Set properties based on passed in shipping rate object. + * + * @param WC_Shipping_Rate $shipping_rate + */ + public function set_shipping_rate( $shipping_rate ) { + $this->set_method_title( $shipping_rate->label ); + $this->set_method_id( $shipping_rate->id ); + $this->set_total( $shipping_rate->cost ); + $this->set_taxes( $shipping_rate->taxes ); + $this->set_meta_data( $shipping_rate->get_meta_data() ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'shipping'; + } + + /** + * Get order item name. + * + * @param string $context + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_method_title( $context ); + } + + /** + * Get title. + * + * @param string $context + * @return string + */ + public function get_method_title( $context = 'view' ) { + $method_title = $this->get_prop( 'method_title', $context ); + if ( 'view' === $context ) { + return $method_title ? $method_title : __( 'Shipping', 'woocommerce' ); + } else { + return $method_title; + } + } + + /** + * Get method ID. + * + * @param string $context + * @return string + */ + public function get_method_id( $context = 'view' ) { + return $this->get_prop( 'method_id', $context ); + } + + /** + * Get total cost. + * + * @param string $context + * @return string + */ + public function get_total( $context = 'view' ) { + return $this->get_prop( 'total', $context ); + } + + /** + * Get total tax. + * + * @param string $context + * @return string + */ + public function get_total_tax( $context = 'view' ) { + return $this->get_prop( 'total_tax', $context ); + } + + /** + * Get taxes. + * + * @param string $context + * @return array + */ + public function get_taxes( $context = 'view' ) { + return $this->get_prop( 'taxes', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compat with legacy arrays. + | + */ + + /** + * offsetGet for ArrayAccess/Backwards compatibility. + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + if ( 'cost' === $offset ) { + $offset = 'total'; + } + return parent::offsetGet( $offset ); + } + + /** + * offsetSet for ArrayAccess/Backwards compatibility. + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + if ( 'cost' === $offset ) { + $offset = 'total'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * offsetExists for ArrayAccess + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'cost' ) ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/includes/class-wc-order-item-tax.php b/includes/class-wc-order-item-tax.php new file mode 100644 index 00000000000..890a3cfea13 --- /dev/null +++ b/includes/class-wc-order-item-tax.php @@ -0,0 +1,263 @@ + '', + 'rate_id' => 0, + 'label' => '', + 'compound' => false, + 'tax_total' => 0, + 'shipping_tax_total' => 0, + ); + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set order item name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_name( $value ) { + $this->set_rate_code( $value ); + } + + /** + * Set item name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_rate_code( $value ) { + $this->set_prop( 'rate_code', wc_clean( $value ) ); + } + + /** + * Set item name. + * @param string $value + * @throws WC_Data_Exception + */ + public function set_label( $value ) { + $this->set_prop( 'label', wc_clean( $value ) ); + } + + /** + * Set tax rate id. + * @param int $value + * @throws WC_Data_Exception + */ + public function set_rate_id( $value ) { + $this->set_prop( 'rate_id', absint( $value ) ); + } + + /** + * Set tax total. + * @param string $value + * @throws WC_Data_Exception + */ + public function set_tax_total( $value ) { + $this->set_prop( 'tax_total', $value ? wc_format_decimal( $value ) : 0 ); + } + + /** + * Set shipping_tax_total + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_tax_total( $value ) { + $this->set_prop( 'shipping_tax_total', $value ? wc_format_decimal( $value ) : 0 ); + } + + /** + * Set compound + * @param bool $value + * @throws WC_Data_Exception + */ + public function set_compound( $value ) { + $this->set_prop( 'compound', (bool) $value ); + } + + /** + * Set properties based on passed in tax rate by ID. + * @param int $tax_rate_id + * @throws WC_Data_Exception + */ + public function set_rate( $tax_rate_id ) { + $tax_rate = WC_Tax::_get_tax_rate( $tax_rate_id, OBJECT ); + + $this->set_rate_id( $tax_rate_id ); + $this->set_rate_code( WC_Tax::get_rate_code( $tax_rate ) ); + $this->set_label( WC_Tax::get_rate_label( $tax_rate ) ); + $this->set_compound( WC_Tax::is_compound( $tax_rate ) ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order item type. + * + * @return string + */ + public function get_type() { + return 'tax'; + } + + /** + * Get rate code/name. + * + * @param string $context + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_rate_code( $context ); + } + + /** + * Get rate code/name. + * + * @param string $context + * @return string + */ + public function get_rate_code( $context = 'view' ) { + return $this->get_prop( 'rate_code', $context ); + } + + /** + * Get label. + * + * @param string $context + * @return string + */ + public function get_label( $context = 'view' ) { + $label = $this->get_prop( 'label', $context ); + if ( 'view' === $context ) { + return $label ? $label : __( 'Tax', 'woocommerce' ); + } else { + return $label; + } + } + + /** + * Get tax rate ID. + * + * @param string $context + * @return int + */ + public function get_rate_id( $context = 'view' ) { + return $this->get_prop( 'rate_id', $context ); + } + + /** + * Get tax_total + * + * @param string $context + * @return string + */ + public function get_tax_total( $context = 'view' ) { + return $this->get_prop( 'tax_total', $context ); + } + + /** + * Get shipping_tax_total + * + * @param string $context + * @return string + */ + public function get_shipping_tax_total( $context = 'view' ) { + return $this->get_prop( 'shipping_tax_total', $context ); + } + + /** + * Get compound. + * + * @param string $context + * @return bool + */ + public function get_compound( $context = 'view' ) { + return $this->get_prop( 'compound', $context ); + } + + /** + * Is this a compound tax rate? + * @return boolean + */ + public function is_compound() { + return $this->get_compound(); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compat with legacy arrays. + | + */ + + /** + * offsetGet for ArrayAccess/Backwards compatibility. + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + if ( 'tax_amount' === $offset ) { + $offset = 'tax_total'; + } elseif ( 'shipping_tax_amount' === $offset ) { + $offset = 'shipping_tax_total'; + } + return parent::offsetGet( $offset ); + } + + /** + * offsetSet for ArrayAccess/Backwards compatibility. + * @deprecated Add deprecation notices in future release. + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + if ( 'tax_amount' === $offset ) { + $offset = 'tax_total'; + } elseif ( 'shipping_tax_amount' === $offset ) { + $offset = 'shipping_tax_total'; + } + parent::offsetSet( $offset, $value ); + } + + /** + * offsetExists for ArrayAccess + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + if ( in_array( $offset, array( 'tax_amount', 'shipping_tax_amount' ) ) ) { + return true; + } + return parent::offsetExists( $offset ); + } +} diff --git a/includes/class-wc-order-item.php b/includes/class-wc-order-item.php new file mode 100644 index 00000000000..e7a68539750 --- /dev/null +++ b/includes/class-wc-order-item.php @@ -0,0 +1,324 @@ + 0, + 'name' => '', + ); + + /** + * Stores meta in cache for future reads. + * A group must be set to to enable caching. + * @var string + */ + protected $cache_group = 'order-items'; + + /** + * Meta type. This should match up with + * the types avaiable at https://codex.wordpress.org/Function_Reference/add_metadata. + * WP defines 'post', 'user', 'comment', and 'term'. + */ + protected $meta_type = 'order_item'; + + /** + * This is the name of this object type. + * @var string + */ + protected $object_type = 'order_item'; + + /** + * Constructor. + * @param int|object|array $item ID to load from the DB, or WC_Order_Item Object + */ + public function __construct( $item = 0 ) { + parent::__construct( $item ); + + if ( $item instanceof WC_Order_Item ) { + $this->set_id( $item->get_id() ); + } elseif ( is_numeric( $item ) && $item > 0 ) { + $this->set_id( $item ); + } else { + $this->set_object_read( true ); + } + + $type = 'line_item' === $this->get_type() ? 'product' : $this->get_type(); + $this->data_store = WC_Data_Store::load( 'order-item-' . $type ); + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get order ID this meta belongs to. + * + * @param string $context + * @return int + */ + public function get_order_id( $context = 'view' ) { + return $this->get_prop( 'order_id', $context ); + } + + /** + * Get order item name. + * + * @param string $context + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_prop( 'name', $context ); + } + + /** + * Get order item type. Overridden by child classes. + * + * @return string + */ + public function get_type() { + return; + } + + /** + * Get quantity. + * @return int + */ + public function get_quantity() { + return 1; + } + + /** + * Get parent order object. + * @return WC_Order + */ + public function get_order() { + return wc_get_order( $this->get_order_id() ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set order ID. + * + * @param int $value + * @throws WC_Data_Exception + */ + public function set_order_id( $value ) { + $this->set_prop( 'order_id', absint( $value ) ); + } + + /** + * Set order item name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_name( $value ) { + $this->set_prop( 'name', wc_clean( $value ) ); + } + + /* + |-------------------------------------------------------------------------- + | Other Methods + |-------------------------------------------------------------------------- + */ + + /** + * Type checking + * @param string|array $type + * @return boolean + */ + public function is_type( $type ) { + return is_array( $type ) ? in_array( $this->get_type(), $type ) : $type === $this->get_type(); + } + + /* + |-------------------------------------------------------------------------- + | Meta Data Handling + |-------------------------------------------------------------------------- + */ + + /** + * Expands things like term slugs before return. + * + * @param string $hideprefix Meta data prefix, (default: _). + * @param bool $include_all Include all meta data, this stop skip items with values already in the product name. + * @return array + */ + public function get_formatted_meta_data( $hideprefix = '_', $include_all = false ) { + $formatted_meta = array(); + $meta_data = $this->get_meta_data(); + $hideprefix_length = ! empty( $hideprefix ) ? strlen( $hideprefix ) : 0; + $product = is_callable( array( $this, 'get_product' ) ) ? $this->get_product() : false; + $order_item_name = $this->get_name(); + + foreach ( $meta_data as $meta ) { + if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hideprefix_length && substr( $meta->key, 0, $hideprefix_length ) === $hideprefix ) ) { + continue; + } + + $meta->key = rawurldecode( (string) $meta->key ); + $meta->value = rawurldecode( (string) $meta->value ); + $attribute_key = str_replace( 'attribute_', '', $meta->key ); + $display_key = wc_attribute_label( $attribute_key, $product ); + $display_value = $meta->value; + + if ( taxonomy_exists( $attribute_key ) ) { + $term = get_term_by( 'slug', $meta->value, $attribute_key ); + if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { + $display_value = $term->name; + } + } + + // Skip items with values already in the product details area of the product name. + if ( ! $include_all && $product && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { + continue; + } + + $formatted_meta[ $meta->id ] = (object) array( + 'key' => $meta->key, + 'value' => $meta->value, + 'display_key' => apply_filters( 'woocommerce_order_item_display_meta_key', $display_key, $meta, $this ), + 'display_value' => wpautop( make_clickable( apply_filters( 'woocommerce_order_item_display_meta_value', $display_value, $meta, $this ) ) ), + ); + } + + return apply_filters( 'woocommerce_order_item_get_formatted_meta_data', $formatted_meta, $this ); + } + + /* + |-------------------------------------------------------------------------- + | Array Access Methods + |-------------------------------------------------------------------------- + | + | For backwards compat with legacy arrays. + | + */ + + /** + * offsetSet for ArrayAccess + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + if ( 'item_meta_array' === $offset ) { + foreach ( $value as $meta_id => $meta ) { + $this->update_meta_data( $meta->key, $meta->value, $meta_id ); + } + return; + } + + if ( array_key_exists( $offset, $this->data ) ) { + $setter = "set_$offset"; + if ( is_callable( array( $this, $setter ) ) ) { + $this->$setter( $value ); + } + return; + } + + $this->update_meta_data( $offset, $value ); + } + + /** + * offsetUnset for ArrayAccess + * @param string $offset + */ + public function offsetUnset( $offset ) { + $this->maybe_read_meta_data(); + + if ( 'item_meta_array' === $offset || 'item_meta' === $offset ) { + $this->meta_data = array(); + return; + } + + if ( array_key_exists( $offset, $this->data ) ) { + unset( $this->data[ $offset ] ); + } + + if ( array_key_exists( $offset, $this->changes ) ) { + unset( $this->changes[ $offset ] ); + } + + $this->delete_meta_data( $offset ); + } + + /** + * offsetExists for ArrayAccess + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + $this->maybe_read_meta_data(); + if ( 'item_meta_array' === $offset || 'item_meta' === $offset || array_key_exists( $offset, $this->data ) ) { + return true; + } + return array_key_exists( $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ) || array_key_exists( '_' . $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ); + } + + /** + * offsetGet for ArrayAccess + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + $this->maybe_read_meta_data(); + + if ( 'item_meta_array' === $offset ) { + $return = array(); + + foreach ( $this->meta_data as $meta ) { + $return[ $meta->id ] = $meta; + } + + return $return; + } + + $meta_values = wp_list_pluck( $this->meta_data, 'value', 'key' ); + + if ( 'item_meta' === $offset ) { + return $meta_values; + } elseif ( 'type' === $offset ) { + return $this->get_type(); + } elseif ( array_key_exists( $offset, $this->data ) ) { + $getter = "get_$offset"; + if ( is_callable( array( $this, $getter ) ) ) { + return $this->$getter(); + } + } elseif ( array_key_exists( '_' . $offset, $meta_values ) ) { + // Item meta was expanded in previous versions, with prefixes removed. This maintains support. + return $meta_values[ '_' . $offset ]; + } elseif ( array_key_exists( $offset, $meta_values ) ) { + return $meta_values[ $offset ]; + } + + return null; + } +} diff --git a/includes/class-wc-order-query.php b/includes/class-wc-order-query.php new file mode 100644 index 00000000000..45408508554 --- /dev/null +++ b/includes/class-wc-order-query.php @@ -0,0 +1,84 @@ + array_keys( wc_get_order_statuses() ), + 'type' => wc_get_order_types( 'view-orders' ), + 'currency' => '', + 'version' => '', + 'prices_include_tax' => '', + 'date_created' => '', + 'date_modified' => '', + 'date_completed' => '', + 'date_paid' => '', + 'discount_total' => '', + 'discount_tax' => '', + 'shipping_total' => '', + 'shipping_tax' => '', + 'cart_tax' => '', + 'total' => '', + 'total_tax' => '', + 'customer' => '', + 'customer_id' => '', + 'order_key' => '', + 'billing_first_name' => '', + 'billing_last_name' => '', + 'billing_company' => '', + 'billing_address_1' => '', + 'billing_address_2' => '', + 'billing_city' => '', + 'billing_state' => '', + 'billing_postcode' => '', + 'billing_country' => '', + 'billing_email' => '', + 'billing_phone' => '', + 'shipping_first_name' => '', + 'shipping_last_name' => '', + 'shipping_company' => '', + 'shipping_address_1' => '', + 'shipping_address_2' => '', + 'shipping_city' => '', + 'shipping_state' => '', + 'shipping_postcode' => '', + 'shipping_country' => '', + 'payment_method' => '', + 'payment_method_title' => '', + 'transaction_id' => '', + 'customer_ip_address' => '', + 'customer_user_agent' => '', + 'created_via' => '', + 'customer_note' => '', + ) + ); + } + + /** + * Get orders matching the current query vars. + * @return array of WC_Order objects + */ + public function get_orders() { + $args = apply_filters( 'woocommerce_order_query_args', $this->get_query_vars() ); + $results = WC_Data_Store::load( 'order' )->query( $this->get_query_vars() ); + return apply_filters( 'woocommerce_order_query', $results, $args ); + } +} diff --git a/includes/class-wc-order-refund.php b/includes/class-wc-order-refund.php new file mode 100644 index 00000000000..88b500d3c9a --- /dev/null +++ b/includes/class-wc-order-refund.php @@ -0,0 +1,197 @@ + '', + 'reason' => '', + 'refunded_by' => 0, + ); + + /** + * Get internal type (post type.) + * @return string + */ + public function get_type() { + return 'shop_order_refund'; + } + + /** + * Get status - always completed for refunds. + * + * @param string $context + * @return string + */ + public function get_status( $context = 'view' ) { + return 'completed'; + } + + /** + * Get a title for the new post type. + */ + public function get_post_title() { + // @codingStandardsIgnoreStart + return sprintf( __( 'Refund – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); + // @codingStandardsIgnoreEnd + } + + /** + * Get refunded amount. + * + * @param string $context + * @return int|float + */ + public function get_amount( $context = 'view' ) { + return $this->get_prop( 'amount', $context ); + } + + /** + * Get refund reason. + * + * @since 2.2 + * @param string $context + * @return int|float + */ + public function get_reason( $context = 'view' ) { + return $this->get_prop( 'reason', $context ); + } + + /** + * Get ID of user who did the refund. + * + * @since 3.0 + * @param string $context + * @return int + */ + public function get_refunded_by( $context = 'view' ) { + return $this->get_prop( 'refunded_by', $context ); + + } + + /** + * Get formatted refunded amount. + * + * @since 2.4 + * @return string + */ + public function get_formatted_refund_amount() { + return apply_filters( 'woocommerce_formatted_refund_amount', wc_price( $this->get_amount(), array( 'currency' => $this->get_currency() ) ), $this ); + } + + /** + * Set refunded amount. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_amount( $value ) { + $this->set_prop( 'amount', wc_format_decimal( $value ) ); + } + + /** + * Set refund reason. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_reason( $value ) { + $this->set_prop( 'reason', $value ); + } + + /** + * Set refunded by. + * + * @param int $value + * @throws WC_Data_Exception + */ + public function set_refunded_by( $value ) { + $this->set_prop( 'refunded_by', absint( $value ) ); + } + + /** + * Magic __get method for backwards compatibility. + * + * @param string $key + * @return mixed + */ + public function __get( $key ) { + wc_doing_it_wrong( $key, 'Refund properties should not be accessed directly.', '3.0' ); + /** + * Maps legacy vars to new getters. + */ + if ( 'reason' === $key ) { + return $this->get_reason(); + } elseif ( 'refund_amount' === $key ) { + return $this->get_amount(); + } + return parent::__get( $key ); + } + + /** + * Gets an refund from the database. + * @deprecated 3.0 + * @param int $id (default: 0). + * @return bool + */ + public function get_refund( $id = 0 ) { + wc_deprecated_function( 'get_refund', '3.0', 'read' ); + if ( ! $id ) { + return false; + } + if ( $result = get_post( $id ) ) { + $this->populate( $result ); + return true; + } + return false; + } + + /** + * Get refund amount. + * @deprecated 3.0 + * @return int|float + */ + public function get_refund_amount() { + wc_deprecated_function( 'get_refund_amount', '3.0', 'get_amount' ); + return $this->get_amount(); + } + + /** + * Get refund reason. + * @deprecated 3.0 + * @return int|float + */ + public function get_refund_reason() { + wc_deprecated_function( 'get_refund_reason', '3.0', 'get_reason' ); + return $this->get_reason(); + } +} diff --git a/includes/class-wc-order.php b/includes/class-wc-order.php index 07a114430c0..32fe17c197c 100644 --- a/includes/class-wc-order.php +++ b/includes/class-wc-order.php @@ -1,18 +1,1818 @@ order_type = 'simple'; - parent::__construct( $order ); + + /** + * Stores data about status changes so relevant hooks can be fired. + * @var bool|array + */ + protected $status_transition = false; + + /** + * Order Data array. This is the core order data exposed in APIs since 3.0.0. + * @since 3.0.0 + * @var array + */ + protected $data = array( + // Abstract order props + 'parent_id' => 0, + 'status' => '', + 'currency' => '', + 'version' => '', + 'prices_include_tax' => false, + 'date_created' => null, + 'date_modified' => null, + 'discount_total' => 0, + 'discount_tax' => 0, + 'shipping_total' => 0, + 'shipping_tax' => 0, + 'cart_tax' => 0, + 'total' => 0, + 'total_tax' => 0, + + // Order props + 'customer_id' => 0, + 'order_key' => '', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + ), + 'payment_method' => '', + 'payment_method_title' => '', + 'transaction_id' => '', + 'customer_ip_address' => '', + 'customer_user_agent' => '', + 'created_via' => '', + 'customer_note' => '', + 'date_completed' => null, + 'date_paid' => null, + 'cart_hash' => '', + ); + + /** + * When a payment is complete this function is called. + * + * Most of the time this should mark an order as 'processing' so that admin can process/post the items. + * If the cart contains only downloadable items then the order is 'completed' since the admin needs to take no action. + * Stock levels are reduced at this point. + * Sales are also recorded for products. + * Finally, record the date of payment. + * + * Order must exist. + * + * @param string $transaction_id Optional transaction id to store in post meta. + * @return bool success + */ + public function payment_complete( $transaction_id = '' ) { + try { + if ( ! $this->get_id() ) { + return false; + } + do_action( 'woocommerce_pre_payment_complete', $this->get_id() ); + + if ( ! empty( WC()->session ) ) { + WC()->session->set( 'order_awaiting_payment', false ); + } + + if ( $this->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_payment_complete', array( 'on-hold', 'pending', 'failed', 'cancelled' ), $this ) ) ) { + if ( ! empty( $transaction_id ) ) { + $this->set_transaction_id( $transaction_id ); + } + if ( ! $this->get_date_paid( 'edit' ) ) { + $this->set_date_paid( current_time( 'timestamp', true ) ); + } + $this->set_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ); + $this->save(); + + do_action( 'woocommerce_payment_complete', $this->get_id() ); + } else { + do_action( 'woocommerce_payment_complete_order_status_' . $this->get_status(), $this->get_id() ); + } + } catch ( Exception $e ) { + return false; + } + return true; } -} \ No newline at end of file + + /** + * Gets order total - formatted for display. + * + * @param string $tax_display Type of tax display. + * @param bool $display_refunded If should include refunded value. + * + * @return string + */ + public function get_formatted_order_total( $tax_display = '', $display_refunded = true ) { + $formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) ); + $order_total = $this->get_total(); + $total_refunded = $this->get_total_refunded(); + $tax_string = ''; + + // Tax for inclusive prices. + if ( wc_tax_enabled() && 'incl' == $tax_display ) { + $tax_string_array = array(); + + if ( 'itemized' == get_option( 'woocommerce_tax_total_display' ) ) { + foreach ( $this->get_tax_totals() as $code => $tax ) { + $tax_amount = ( $total_refunded && $display_refunded ) ? wc_price( WC_Tax::round( $tax->amount - $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), array( 'currency' => $this->get_currency() ) ) : $tax->formatted_amount; + $tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label ); + } + } else { + $tax_amount = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax(); + $tax_string_array[] = sprintf( '%s %s', wc_price( $tax_amount, array( 'currency' => $this->get_currency() ) ), WC()->countries->tax_or_vat() ); + } + if ( ! empty( $tax_string_array ) ) { + $tax_string = ' ' . sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) . ''; + } + } + + if ( $total_refunded && $display_refunded ) { + $formatted_total = '' . strip_tags( $formatted_total ) . ' ' . wc_price( $order_total - $total_refunded, array( 'currency' => $this->get_currency() ) ) . $tax_string . ''; + } else { + $formatted_total .= $tax_string; + } + + /** + * Filter WooCommerce formatted order total. + * + * @param string $formatted_total Total to display. + * @param WC_Order $order Order data. + * @param string $tax_display Type of tax display. + * @param bool $display_refunded If should include refunded value. + */ + return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this, $tax_display, $display_refunded ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + | + | Methods which create, read, update and delete orders from the database. + | Written in abstract fashion so that the way orders are stored can be + | changed more easily in the future. + | + | A save method is included for convenience (chooses update or create based + | on if the order exists yet). + | + */ + + /** + * Save data to the database. + * + * @since 3.0.0 + * @return int order ID + */ + public function save() { + $this->maybe_set_user_billing_email(); + if ( $this->data_store ) { + // Trigger action before saving to the DB. Allows you to adjust object props before save. + do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); + + if ( $this->get_id() ) { + $this->data_store->update( $this ); + } else { + $this->data_store->create( $this ); + } + } + $this->save_items(); + $this->status_transition(); + return $this->get_id(); + } + + /** + * Set order status. + * @since 3.0.0 + * + * @param string $new_status Status to change the order to. No internal wc- prefix is required. + * @param string $note (default: '') Optional note to add. + * @param bool $manual_update is this a manual order status change? + * @param array details of change + * + * @return array + */ + public function set_status( $new_status, $note = '', $manual_update = false ) { + $result = parent::set_status( $new_status ); + + if ( true === $this->object_read && ! empty( $result['from'] ) && $result['from'] !== $result['to'] ) { + $this->status_transition = array( + 'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $result['from'], + 'to' => $result['to'], + 'note' => $note, + 'manual' => (bool) $manual_update, + ); + + $this->maybe_set_date_paid(); + $this->maybe_set_date_completed(); + } + + return $result; + } + + /** + * Maybe set date paid. + * + * Sets the date paid variable when transitioning to the payment complete + * order status. This is either processing or completed. This is not filtered + * to avoid infinite loops e.g. if loading an order via the filter. + * + * Date paid is set once in this manner - only when it is not already set. + * This ensures the data exists even if a gateway does not use the + * `payment_complete` method. + * + * @since 3.0.0 + */ + public function maybe_set_date_paid() { + if ( ! $this->get_date_paid( 'edit' ) && $this->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ) ) { + $this->set_date_paid( current_time( 'timestamp' ) ); + } + } + + /** + * Maybe set date completed. + * + * Sets the date completed variable when transitioning to completed status. + * + * @since 3.0.0 + */ + protected function maybe_set_date_completed() { + if ( $this->has_status( 'completed' ) ) { + $this->set_date_completed( current_time( 'timestamp', true ) ); + } + } + + /** + * Updates status of order immediately. Order must exist. + * @uses WC_Order::set_status() + * + * @param string $new_status + * @param string $note + * @param bool $manual + * + * @return bool success + */ + public function update_status( $new_status, $note = '', $manual = false ) { + try { + if ( ! $this->get_id() ) { + return false; + } + $this->set_status( $new_status, $note, $manual ); + $this->save(); + } catch ( Exception $e ) { + return false; + } + return true; + } + + /** + * Handle the status transition. + */ + protected function status_transition() { + if ( $this->status_transition ) { + do_action( 'woocommerce_order_status_' . $this->status_transition['to'], $this->get_id(), $this ); + + if ( ! empty( $this->status_transition['from'] ) ) { + /* translators: 1: old order status 2: new order status */ + $transition_note = sprintf( __( 'Order status changed from %1$s to %2$s.', 'woocommerce' ), wc_get_order_status_name( $this->status_transition['from'] ), wc_get_order_status_name( $this->status_transition['to'] ) ); + + do_action( 'woocommerce_order_status_' . $this->status_transition['from'] . '_to_' . $this->status_transition['to'], $this->get_id(), $this ); + do_action( 'woocommerce_order_status_changed', $this->get_id(), $this->status_transition['from'], $this->status_transition['to'], $this ); + } else { + /* translators: %s: new order status */ + $transition_note = sprintf( __( 'Order status set to %s.', 'woocommerce' ), wc_get_order_status_name( $this->status_transition['to'] ) ); + } + + // Note the transition occurred + $this->add_order_note( trim( $this->status_transition['note'] . ' ' . $transition_note ), 0, $this->status_transition['manual'] ); + + // This has ran, so reset status transition variable + $this->status_transition = false; + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the order object. + | + */ + + /** + * Get all class data in array format. + * @since 3.0.0 + * @return array + */ + public function get_data() { + return array_merge( + array( + 'id' => $this->get_id(), + ), + $this->data, + array( + 'number' => $this->get_order_number(), + 'meta_data' => $this->get_meta_data(), + 'line_items' => $this->get_items( 'line_item' ), + 'tax_lines' => $this->get_items( 'tax' ), + 'shipping_lines' => $this->get_items( 'shipping' ), + 'fee_lines' => $this->get_items( 'fee' ), + 'coupon_lines' => $this->get_items( 'coupon' ), + ) + ); + } + + /** + * Expands the shipping and billing information in the changes array. + */ + public function get_changes() { + $changed_props = parent::get_changes(); + $subs = array( 'shipping', 'billing' ); + foreach ( $subs as $sub ) { + if ( ! empty( $changed_props[ $sub ] ) ) { + foreach ( $changed_props[ $sub ] as $sub_prop => $value ) { + $changed_props[ $sub . '_' . $sub_prop ] = $value; + } + } + } + if ( isset( $changed_props['customer_note'] ) ) { + $changed_props['post_excerpt'] = $changed_props['customer_note']; + } + return $changed_props; + } + + /** + * get_order_number function. + * + * Gets the order number for display (by default, order ID). + * + * @return string + */ + public function get_order_number() { + return (string) apply_filters( 'woocommerce_order_number', $this->get_id(), $this ); + } + + /** + * Get order key. + * + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_order_key( $context = 'view' ) { + return $this->get_prop( 'order_key', $context ); + } + + /** + * Get customer_id. + * + * @param string $context + * @return int + */ + public function get_customer_id( $context = 'view' ) { + return $this->get_prop( 'customer_id', $context ); + } + + /** + * Alias for get_customer_id(). + * + * @param string $context + * @return int + */ + public function get_user_id( $context = 'view' ) { + return $this->get_customer_id( $context ); + } + + /** + * Get the user associated with the order. False for guests. + * + * @return WP_User|false + */ + public function get_user() { + return $this->get_user_id() ? get_user_by( 'id', $this->get_user_id() ) : false; + } + + /** + * Gets a prop for a getter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to get. + * @param string $address billing or shipping. + * @param string $context What the value is for. Valid values are view and edit. + * @return mixed + */ + protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { + $value = null; + + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); + } + } + return $value; + } + + /** + * Get billing_first_name. + * + * @param string $context + * @return string + */ + public function get_billing_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'billing', $context ); + } + + /** + * Get billing_last_name. + * + * @param string $context + * @return string + */ + public function get_billing_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'billing', $context ); + } + + /** + * Get billing_company. + * + * @param string $context + * @return string + */ + public function get_billing_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'billing', $context ); + } + + /** + * Get billing_address_1. + * + * @param string $context + * @return string + */ + public function get_billing_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'billing', $context ); + } + + /** + * Get billing_address_2. + * + * @param string $context + * @return string $value + */ + public function get_billing_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'billing', $context ); + } + + /** + * Get billing_city. + * + * @param string $context + * @return string $value + */ + public function get_billing_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'billing', $context ); + } + + /** + * Get billing_state. + * + * @param string $context + * @return string + */ + public function get_billing_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'billing', $context ); + } + + /** + * Get billing_postcode. + * + * @param string $context + * @return string + */ + public function get_billing_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'billing', $context ); + } + + /** + * Get billing_country. + * + * @param string $context + * @return string + */ + public function get_billing_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'billing', $context ); + } + + /** + * Get billing_email. + * + * @param string $context + * @return string + */ + public function get_billing_email( $context = 'view' ) { + return $this->get_address_prop( 'email', 'billing', $context ); + } + + /** + * Get billing_phone. + * + * @param string $context + * @return string + */ + public function get_billing_phone( $context = 'view' ) { + return $this->get_address_prop( 'phone', 'billing', $context ); + } + + /** + * Get shipping_first_name. + * + * @param string $context + * @return string + */ + public function get_shipping_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'shipping', $context ); + } + + /** + * Get shipping_last_name. + * + * @param string $context + * @return string + */ + public function get_shipping_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'shipping', $context ); + } + + /** + * Get shipping_company. + * + * @param string $context + * @return string + */ + public function get_shipping_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'shipping', $context ); + } + + /** + * Get shipping_address_1. + * + * @param string $context + * @return string + */ + public function get_shipping_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'shipping', $context ); + } + + /** + * Get shipping_address_2. + * + * @param string $context + * @return string + */ + public function get_shipping_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'shipping', $context ); + } + + /** + * Get shipping_city. + * + * @param string $context + * @return string + */ + public function get_shipping_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'shipping', $context ); + } + + /** + * Get shipping_state. + * + * @param string $context + * @return string + */ + public function get_shipping_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'shipping', $context ); + } + + /** + * Get shipping_postcode. + * + * @param string $context + * @return string + */ + public function get_shipping_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'shipping', $context ); + } + + /** + * Get shipping_country. + * + * @param string $context + * @return string + */ + public function get_shipping_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'shipping', $context ); + } + + /** + * Get the payment method. + * + * @param string $context + * @return string + */ + public function get_payment_method( $context = 'view' ) { + return $this->get_prop( 'payment_method', $context ); + } + + /** + * Get payment_method_title. + * + * @param string $context + * @return string + */ + public function get_payment_method_title( $context = 'view' ) { + return $this->get_prop( 'payment_method_title', $context ); + } + + /** + * Get transaction_id. + * + * @param string $context + * @return string + */ + public function get_transaction_id( $context = 'view' ) { + return $this->get_prop( 'transaction_id', $context ); + } + + /** + * Get customer_ip_address. + * + * @param string $context + * @return string + */ + public function get_customer_ip_address( $context = 'view' ) { + return $this->get_prop( 'customer_ip_address', $context ); + } + + /** + * Get customer_user_agent. + * + * @param string $context + * @return string + */ + public function get_customer_user_agent( $context = 'view' ) { + return $this->get_prop( 'customer_user_agent', $context ); + } + + /** + * Get created_via. + * + * @param string $context + * @return string + */ + public function get_created_via( $context = 'view' ) { + return $this->get_prop( 'created_via', $context ); + } + + /** + * Get customer_note. + * + * @param string $context + * @return string + */ + public function get_customer_note( $context = 'view' ) { + return $this->get_prop( 'customer_note', $context ); + } + + /** + * Get date_completed. + * + * @param string $context + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_completed( $context = 'view' ) { + return $this->get_prop( 'date_completed', $context ); + } + + /** + * Get date_paid. + * + * @param string $context + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_paid( $context = 'view' ) { + $date_paid = $this->get_prop( 'date_paid', $context ); + + if ( 'view' === $context && ! $date_paid && version_compare( $this->get_version( 'edit' ), '3.0', '<' ) && $this->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ) ) { + // In view context, return a date if missing. + $date_paid = $this->get_date_created( 'edit' ); + } + return $date_paid; + } + + /** + * Get cart hash. + * + * @param string $context + * @return string + */ + public function get_cart_hash( $context = 'view' ) { + return $this->get_prop( 'cart_hash', $context ); + } + + /** + * Returns the requested address in raw, non-formatted way. + * Note: Merges raw data with get_prop data so changes are returned too. + * + * @since 2.4.0 + * @param string $type Billing or shipping. Anything else besides 'billing' will return shipping address. + * @return array The stored address after filter. + */ + public function get_address( $type = 'billing' ) { + return apply_filters( 'woocommerce_get_order_address', array_merge( $this->data[ $type ], $this->get_prop( $type, 'view' ) ), $type, $this ); + } + + /** + * Get a formatted shipping address for the order. + * + * @return string + */ + public function get_shipping_address_map_url() { + $address = $this->get_address( 'shipping' ); + + // Remove name and company before generate the Google Maps URL. + unset( $address['first_name'], $address['last_name'], $address['company'] ); + + $address = apply_filters( 'woocommerce_shipping_address_map_url_parts', $address, $this ); + + return apply_filters( 'woocommerce_shipping_address_map_url', 'https://maps.google.com/maps?&q=' . urlencode( implode( ', ', $address ) ) . '&z=16', $this ); + } + + /** + * Get a formatted billing full name. + * + * @return string + */ + public function get_formatted_billing_full_name() { + /* translators: 1: first name 2: last name */ + return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_billing_first_name(), $this->get_billing_last_name() ); + } + + /** + * Get a formatted shipping full name. + * + * @return string + */ + public function get_formatted_shipping_full_name() { + /* translators: 1: first name 2: last name */ + return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_shipping_first_name(), $this->get_shipping_last_name() ); + } + + /** + * Get a formatted billing address for the order. + * + * @return string + */ + public function get_formatted_billing_address() { + return WC()->countries->get_formatted_address( apply_filters( 'woocommerce_order_formatted_billing_address', $this->get_address( 'billing' ), $this ) ); + } + + /** + * Get a formatted shipping address for the order. + * + * @return string + */ + public function get_formatted_shipping_address() { + if ( $this->has_shipping_address() ) { + return WC()->countries->get_formatted_address( apply_filters( 'woocommerce_order_formatted_shipping_address', $this->get_address( 'shipping' ), $this ) ); + } else { + return ''; + } + } + + /** + * Returns true if the order has a shipping address. + * + * @since 3.0.4 + * @return boolean + */ + public function has_shipping_address() { + return $this->get_shipping_address_1() || $this->get_shipping_address_2(); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Functions for setting order data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. However, for backwards compatibility pre 3.0.0 some of these + | setters may handle both. + | + */ + + /** + * Sets a prop for a setter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to set. + * @param string $address Name of address to set. billing or shipping. + * @param mixed $value Value of the prop. + */ + protected function set_address_prop( $prop, $address = 'billing', $value ) { + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + if ( true === $this->object_read ) { + if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { + $this->changes[ $address ][ $prop ] = $value; + } + } else { + $this->data[ $address ][ $prop ] = $value; + } + } + } + + /** + * Set order_key. + * + * @param string $value Max length 22 chars. + * @throws WC_Data_Exception + */ + public function set_order_key( $value ) { + $this->set_prop( 'order_key', substr( $value, 0, 22 ) ); + } + + /** + * Set customer_id. + * + * @param int $value + * @throws WC_Data_Exception + */ + public function set_customer_id( $value ) { + $this->set_prop( 'customer_id', absint( $value ) ); + } + + /** + * Set billing_first_name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_first_name( $value ) { + $this->set_address_prop( 'first_name', 'billing', $value ); + } + + /** + * Set billing_last_name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_last_name( $value ) { + $this->set_address_prop( 'last_name', 'billing', $value ); + } + + /** + * Set billing_company. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_company( $value ) { + $this->set_address_prop( 'company', 'billing', $value ); + } + + /** + * Set billing_address_1. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_address_1( $value ) { + $this->set_address_prop( 'address_1', 'billing', $value ); + } + + /** + * Set billing_address_2. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_address_2( $value ) { + $this->set_address_prop( 'address_2', 'billing', $value ); + } + + /** + * Set billing_city. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_city( $value ) { + $this->set_address_prop( 'city', 'billing', $value ); + } + + /** + * Set billing_state. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_state( $value ) { + $this->set_address_prop( 'state', 'billing', $value ); + } + + /** + * Set billing_postcode. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_postcode( $value ) { + $this->set_address_prop( 'postcode', 'billing', $value ); + } + + /** + * Set billing_country. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_country( $value ) { + $this->set_address_prop( 'country', 'billing', $value ); + } + + /** + * Maybe set empty billing email to that of the user who owns the order. + */ + protected function maybe_set_user_billing_email() { + if ( ! $this->get_billing_email() && ( $user = $this->get_user() ) ) { + try { + $this->set_billing_email( $user->user_email ); + } catch( WC_Data_Exception $e ) { + unset( $e ); + } + } + } + + /** + * Set billing_email. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_email( $value ) { + if ( $value && ! is_email( $value ) ) { + $this->error( 'order_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); + } + $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); + } + + /** + * Set billing_phone. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_billing_phone( $value ) { + $this->set_address_prop( 'phone', 'billing', $value ); + } + + /** + * Set shipping_first_name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_first_name( $value ) { + $this->set_address_prop( 'first_name', 'shipping', $value ); + } + + /** + * Set shipping_last_name. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_last_name( $value ) { + $this->set_address_prop( 'last_name', 'shipping', $value ); + } + + /** + * Set shipping_company. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_company( $value ) { + $this->set_address_prop( 'company', 'shipping', $value ); + } + + /** + * Set shipping_address_1. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_address_1( $value ) { + $this->set_address_prop( 'address_1', 'shipping', $value ); + } + + /** + * Set shipping_address_2. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_address_2( $value ) { + $this->set_address_prop( 'address_2', 'shipping', $value ); + } + + /** + * Set shipping_city. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_city( $value ) { + $this->set_address_prop( 'city', 'shipping', $value ); + } + + /** + * Set shipping_state. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_state( $value ) { + $this->set_address_prop( 'state', 'shipping', $value ); + } + + /** + * Set shipping_postcode. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_postcode( $value ) { + $this->set_address_prop( 'postcode', 'shipping', $value ); + } + + /** + * Set shipping_country. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_shipping_country( $value ) { + $this->set_address_prop( 'country', 'shipping', $value ); + } + + /** + * Set the payment method. + * + * @param string $payment_method Supports WC_Payment_Gateway for bw compatibility with < 3.0 + * @throws WC_Data_Exception + */ + public function set_payment_method( $payment_method = '' ) { + if ( is_object( $payment_method ) ) { + $this->set_payment_method( $payment_method->id ); + $this->set_payment_method_title( $payment_method->get_title() ); + } elseif ( '' === $payment_method ) { + $this->set_prop( 'payment_method', '' ); + $this->set_prop( 'payment_method_title', '' ); + } else { + $this->set_prop( 'payment_method', $payment_method ); + } + } + + /** + * Set payment_method_title. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_payment_method_title( $value ) { + $this->set_prop( 'payment_method_title', $value ); + } + + /** + * Set transaction_id. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_transaction_id( $value ) { + $this->set_prop( 'transaction_id', $value ); + } + + /** + * Set customer_ip_address. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_customer_ip_address( $value ) { + $this->set_prop( 'customer_ip_address', $value ); + } + + /** + * Set customer_user_agent. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_customer_user_agent( $value ) { + $this->set_prop( 'customer_user_agent', $value ); + } + + /** + * Set created_via. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_created_via( $value ) { + $this->set_prop( 'created_via', $value ); + } + + /** + * Set customer_note. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_customer_note( $value ) { + $this->set_prop( 'customer_note', $value ); + } + + /** + * Set date_completed. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + * @throws WC_Data_Exception + */ + public function set_date_completed( $date = null ) { + $this->set_date_prop( 'date_completed', $date ); + } + + /** + * Set date_paid. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + * @throws WC_Data_Exception + */ + public function set_date_paid( $date = null ) { + $this->set_date_prop( 'date_paid', $date ); + } + + /** + * Set cart hash. + * + * @param string $value + * @throws WC_Data_Exception + */ + public function set_cart_hash( $value ) { + $this->set_prop( 'cart_hash', $value ); + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + | + | Checks if a condition is true or false. + | + */ + + /** + * Check if an order key is valid. + * + * @param mixed $key + * @return bool + */ + public function key_is_valid( $key ) { + return $key === $this->get_order_key(); + } + + /** + * See if order matches cart_hash. + * + * @param string $cart_hash + * + * @return bool + */ + public function has_cart_hash( $cart_hash = '' ) { + return hash_equals( $this->get_cart_hash(), $cart_hash ); + } + + /** + * Checks if an order can be edited, specifically for use on the Edit Order screen. + * @return bool + */ + public function is_editable() { + return apply_filters( 'wc_order_is_editable', in_array( $this->get_status(), array( 'pending', 'on-hold', 'auto-draft' ) ), $this ); + } + + /** + * Returns if an order has been paid for based on the order status. + * @since 2.5.0 + * @return bool + */ + public function is_paid() { + return apply_filters( 'woocommerce_order_is_paid', $this->has_status( wc_get_is_paid_statuses() ), $this ); + } + + /** + * Checks if product download is permitted. + * + * @return bool + */ + public function is_download_permitted() { + return apply_filters( 'woocommerce_order_is_download_permitted', $this->has_status( 'completed' ) || ( 'yes' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) && $this->has_status( 'processing' ) ), $this ); + } + + /** + * Checks if an order needs display the shipping address, based on shipping method. + * @return bool + */ + public function needs_shipping_address() { + if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { + return false; + } + + $hide = apply_filters( 'woocommerce_order_hide_shipping_address', array( 'local_pickup' ), $this ); + $needs_address = false; + + foreach ( $this->get_shipping_methods() as $shipping_method ) { + // Remove any instance IDs after : + $shipping_method_id = current( explode( ':', $shipping_method['method_id'] ) ); + + if ( ! in_array( $shipping_method_id, $hide ) ) { + $needs_address = true; + break; + } + } + + return apply_filters( 'woocommerce_order_needs_shipping_address', $needs_address, $hide, $this ); + } + + /** + * Returns true if the order contains a downloadable product. + * @return bool + */ + public function has_downloadable_item() { + foreach ( $this->get_items() as $item ) { + if ( $item->is_type( 'line_item' ) && ( $product = $item->get_product() ) && $product->is_downloadable() && $product->has_file() ) { + return true; + } + } + return false; + } + + /** + * Checks if an order needs payment, based on status and order total. + * + * @return bool + */ + public function needs_payment() { + $valid_order_statuses = apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this ); + return apply_filters( 'woocommerce_order_needs_payment', ( $this->has_status( $valid_order_statuses ) && $this->get_total() > 0 ), $this, $valid_order_statuses ); + } + + /** + * See if the order needs processing before it can be completed. + * + * Orders which only contain virtual, downloadable items do not need admin + * intervention. + * + * @since 3.0.0 + * @return bool + */ + public function needs_processing() { + $needs_processing = false; + + if ( sizeof( $this->get_items() ) > 0 ) { + foreach ( $this->get_items() as $item ) { + if ( $item->is_type( 'line_item' ) && ( $product = $item->get_product() ) ) { + $virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual(); + + if ( apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) { + $needs_processing = true; + break; + } + } + } + } + + return $needs_processing; + } + + /* + |-------------------------------------------------------------------------- + | URLs and Endpoints + |-------------------------------------------------------------------------- + */ + + /** + * Generates a URL so that a customer can pay for their (unpaid - pending) order. Pass 'true' for the checkout version which doesn't offer gateway choices. + * + * @param bool $on_checkout + * @return string + */ + public function get_checkout_payment_url( $on_checkout = false ) { + $pay_url = wc_get_endpoint_url( 'order-pay', $this->get_id(), wc_get_page_permalink( 'checkout' ) ); + + if ( 'yes' == get_option( 'woocommerce_force_ssl_checkout' ) || is_ssl() ) { + $pay_url = str_replace( 'http:', 'https:', $pay_url ); + } + + if ( $on_checkout ) { + $pay_url = add_query_arg( 'key', $this->get_order_key(), $pay_url ); + } else { + $pay_url = add_query_arg( array( 'pay_for_order' => 'true', 'key' => $this->get_order_key() ), $pay_url ); + } + + return apply_filters( 'woocommerce_get_checkout_payment_url', $pay_url, $this ); + } + + /** + * Generates a URL for the thanks page (order received). + * + * @return string + */ + public function get_checkout_order_received_url() { + $order_received_url = wc_get_endpoint_url( 'order-received', $this->get_id(), wc_get_page_permalink( 'checkout' ) ); + + if ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || is_ssl() ) { + $order_received_url = str_replace( 'http:', 'https:', $order_received_url ); + } + + $order_received_url = add_query_arg( 'key', $this->get_order_key(), $order_received_url ); + + return apply_filters( 'woocommerce_get_checkout_order_received_url', $order_received_url, $this ); + } + + /** + * Generates a URL so that a customer can cancel their (unpaid - pending) order. + * + * @param string $redirect + * + * @return string + */ + public function get_cancel_order_url( $redirect = '' ) { + return apply_filters( 'woocommerce_get_cancel_order_url', wp_nonce_url( add_query_arg( array( + 'cancel_order' => 'true', + 'order' => $this->get_order_key(), + 'order_id' => $this->get_id(), + 'redirect' => $redirect, + ), $this->get_cancel_endpoint() ), 'woocommerce-cancel_order' ) ); + } + + /** + * Generates a raw (unescaped) cancel-order URL for use by payment gateways. + * + * @param string $redirect + * + * @return string The unescaped cancel-order URL. + */ + public function get_cancel_order_url_raw( $redirect = '' ) { + return apply_filters( 'woocommerce_get_cancel_order_url_raw', add_query_arg( array( + 'cancel_order' => 'true', + 'order' => $this->get_order_key(), + 'order_id' => $this->get_id(), + 'redirect' => $redirect, + '_wpnonce' => wp_create_nonce( 'woocommerce-cancel_order' ), + ), $this->get_cancel_endpoint() ) ); + } + + /** + * Helper method to return the cancel endpoint. + * + * @return string the cancel endpoint; either the cart page or the home page. + */ + public function get_cancel_endpoint() { + $cancel_endpoint = wc_get_page_permalink( 'cart' ); + if ( ! $cancel_endpoint ) { + $cancel_endpoint = home_url(); + } + + if ( false === strpos( $cancel_endpoint, '?' ) ) { + $cancel_endpoint = trailingslashit( $cancel_endpoint ); + } + + return $cancel_endpoint; + } + + /** + * Generates a URL to view an order from the my account page. + * + * @return string + */ + public function get_view_order_url() { + return apply_filters( 'woocommerce_get_view_order_url', wc_get_endpoint_url( 'view-order', $this->get_id(), wc_get_page_permalink( 'myaccount' ) ), $this ); + } + + /* + |-------------------------------------------------------------------------- + | Order notes. + |-------------------------------------------------------------------------- + */ + + /** + * Adds a note (comment) to the order. Order must exist. + * + * @param string $note Note to add. + * @param int $is_customer_note (default: 0) Is this a note for the customer? + * @param bool added_by_user Was the note added by a user? + * @return int Comment ID. + */ + public function add_order_note( $note, $is_customer_note = 0, $added_by_user = false ) { + if ( ! $this->get_id() ) { + return 0; + } + + if ( is_user_logged_in() && current_user_can( 'edit_shop_order', $this->get_id() ) && $added_by_user ) { + $user = get_user_by( 'id', get_current_user_id() ); + $comment_author = $user->display_name; + $comment_author_email = $user->user_email; + } else { + $comment_author = __( 'WooCommerce', 'woocommerce' ); + $comment_author_email = strtolower( __( 'WooCommerce', 'woocommerce' ) ) . '@'; + $comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', $_SERVER['HTTP_HOST'] ) : 'noreply.com'; + $comment_author_email = sanitize_email( $comment_author_email ); + } + $commentdata = apply_filters( 'woocommerce_new_order_note_data', array( + 'comment_post_ID' => $this->get_id(), + 'comment_author' => $comment_author, + 'comment_author_email' => $comment_author_email, + 'comment_author_url' => '', + 'comment_content' => $note, + 'comment_agent' => 'WooCommerce', + 'comment_type' => 'order_note', + 'comment_parent' => 0, + 'comment_approved' => 1, + ), array( 'order_id' => $this->get_id(), 'is_customer_note' => $is_customer_note ) ); + + $comment_id = wp_insert_comment( $commentdata ); + + if ( $is_customer_note ) { + add_comment_meta( $comment_id, 'is_customer_note', 1 ); + + do_action( 'woocommerce_new_customer_note', array( 'order_id' => $this->get_id(), 'customer_note' => $commentdata['comment_content'] ) ); + } + + return $comment_id; + } + + /** + * List order notes (public) for the customer. + * + * @return array + */ + public function get_customer_order_notes() { + $notes = array(); + $args = array( + 'post_id' => $this->get_id(), + 'approve' => 'approve', + 'type' => '', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); + + $comments = get_comments( $args ); + + foreach ( $comments as $comment ) { + if ( ! get_comment_meta( $comment->comment_ID, 'is_customer_note', true ) ) { + continue; + } + $comment->comment_content = make_clickable( $comment->comment_content ); + $notes[] = $comment; + } + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); + + return $notes; + } + + /* + |-------------------------------------------------------------------------- + | Refunds + |-------------------------------------------------------------------------- + */ + + /** + * Get order refunds. + * + * @since 2.2 + * @return array of WC_Order_Refund objects + */ + public function get_refunds() { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $this->get_id(); + $cached_data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + $this->refunds = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $this->get_id(), + 'limit' => -1, + ) ); + + wp_cache_set( $cache_key, $this->refunds, $this->cache_group ); + + return $this->refunds; + } + + /** + * Get amount already refunded. + * + * @since 2.2 + * @return string + */ + public function get_total_refunded() { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_refunded' . $this->get_id(); + $cached_data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + $total_refunded = $this->data_store->get_total_refunded( $this ); + + wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); + + return $total_refunded; + } + + /** + * Get the total tax refunded. + * + * @since 2.3 + * @return float + */ + public function get_total_tax_refunded() { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_tax_refunded' . $this->get_id(); + $cached_data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + $total_refunded = $this->data_store->get_total_tax_refunded( $this ); + + wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); + + return $total_refunded; + } + + /** + * Get the total shipping refunded. + * + * @since 2.4 + * @return float + */ + public function get_total_shipping_refunded() { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_shipping_refunded' . $this->get_id(); + $cached_data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + $total_refunded = $this->data_store->get_total_shipping_refunded( $this ); + + wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); + + return $total_refunded; + } + + /** + * Gets the count of order items of a certain type that have been refunded. + * @since 2.4.0 + * @param string $item_type + * @return string + */ + public function get_item_count_refunded( $item_type = '' ) { + if ( empty( $item_type ) ) { + $item_type = array( 'line_item' ); + } + if ( ! is_array( $item_type ) ) { + $item_type = array( $item_type ); + } + $count = 0; + + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + $count += abs( $refunded_item->get_quantity() ); + } + } + + return apply_filters( 'woocommerce_get_item_count_refunded', $count, $item_type, $this ); + } + + /** + * Get the total number of items refunded. + * + * @since 2.4.0 + * @param string $item_type type of the item we're checking, if not a line_item + * @return integer + */ + public function get_total_qty_refunded( $item_type = 'line_item' ) { + $qty = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + $qty += $refunded_item->get_quantity(); + } + } + return $qty; + } + + /** + * Get the refunded amount for a line item. + * + * @param int $item_id ID of the item we're checking + * @param string $item_type type of the item we're checking, if not a line_item + * @return integer + */ + public function get_qty_refunded_for_item( $item_id, $item_type = 'line_item' ) { + $qty = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { + $qty += $refunded_item->get_quantity(); + } + } + } + return $qty; + } + + /** + * Get the refunded amount for a line item. + * + * @param int $item_id ID of the item we're checking + * @param string $item_type type of the item we're checking, if not a line_item + * @return integer + */ + public function get_total_refunded_for_item( $item_id, $item_type = 'line_item' ) { + $total = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { + $total += $refunded_item->get_total(); + } + } + } + return $total * -1; + } + + /** + * Get the refunded tax amount for a line item. + * + * @param int $item_id ID of the item we're checking + * @param int $tax_id ID of the tax we're checking + * @param string $item_type type of the item we're checking, if not a line_item + * @return double + */ + public function get_tax_refunded_for_item( $item_id, $tax_id, $item_type = 'line_item' ) { + $total = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( $item_type ) as $refunded_item ) { + $refunded_item_id = (int) $refunded_item->get_meta( '_refunded_item_id' ); + if ( $refunded_item_id === $item_id ) { + $taxes = $refunded_item->get_taxes(); + $total += isset( $taxes['total'][ $tax_id ] ) ? $taxes['total'][ $tax_id ] : 0; + break; + } + } + } + return wc_round_tax_total( $total ) * -1; + } + + /** + * Get total tax refunded by rate ID. + * + * @param int $rate_id + * + * @return float + */ + public function get_total_tax_refunded_by_rate_id( $rate_id ) { + $total = 0; + foreach ( $this->get_refunds() as $refund ) { + foreach ( $refund->get_items( 'tax' ) as $refunded_item ) { + if ( absint( $refunded_item->get_rate_id() ) === $rate_id ) { + $total += abs( $refunded_item->get_tax_total() ) + abs( $refunded_item->get_shipping_tax_total() ); + } + } + } + + return $total; + } + + /** + * How much money is left to refund? + * @return string + */ + public function get_remaining_refund_amount() { + return wc_format_decimal( $this->get_total() - $this->get_total_refunded(), wc_get_price_decimals() ); + } + + /** + * How many items are left to refund? + * @return int + */ + public function get_remaining_refund_items() { + return absint( $this->get_item_count() - $this->get_item_count_refunded() ); + } + + /** + * Add total row for the payment method. + * + * @param array $total_rows + * @param string $tax_display + */ + protected function add_order_item_totals_payment_method_row( &$total_rows, $tax_display ) { + if ( $this->get_total() > 0 && $this->get_payment_method_title() ) { + $total_rows['payment_method'] = array( + 'label' => __( 'Payment method:', 'woocommerce' ), + 'value' => $this->get_payment_method_title(), + ); + } + } + + /** + * Add total row for refunds. + * + * @param array $total_rows + * @param string $tax_display + */ + protected function add_order_item_totals_refund_rows( &$total_rows, $tax_display ) { + if ( $refunds = $this->get_refunds() ) { + foreach ( $refunds as $id => $refund ) { + $total_rows[ 'refund_' . $id ] = array( + 'label' => $refund->get_reason() ? $refund->get_reason() : __( 'Refund', 'woocommerce' ) . ':', + 'value' => wc_price( '-' . $refund->get_amount(), array( 'currency' => $this->get_currency() ) ), + ); + } + } + } + + /** + * Get totals for display on pages and in emails. + * + * @param mixed $tax_display + * @return array + */ + public function get_order_item_totals( $tax_display = '' ) { + $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); + $total_rows = array(); + + $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); + $this->add_order_item_totals_discount_row( $total_rows, $tax_display ); + $this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); + $this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); + $this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); + $this->add_order_item_totals_payment_method_row( $total_rows, $tax_display ); + $this->add_order_item_totals_refund_rows( $total_rows, $tax_display ); + $this->add_order_item_totals_total_row( $total_rows, $tax_display ); + + return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display ); + } +} diff --git a/includes/class-wc-payment-gateways.php b/includes/class-wc-payment-gateways.php index 052efb68672..46fc76063a7 100644 --- a/includes/class-wc-payment-gateways.php +++ b/includes/class-wc-payment-gateways.php @@ -1,11 +1,16 @@ init(); } - /** - * Load gateways and hook in functions. - * - * @access public - * @return void - */ - function init() { - - $load_gateways = apply_filters( 'woocommerce_payment_gateways', array( - 'WC_Gateway_BACS', + /** + * Load gateways and hook in functions. + */ + public function init() { + $load_gateways = array( + 'WC_Gateway_BACS', 'WC_Gateway_Cheque', 'WC_Gateway_COD', - 'WC_Gateway_Mijireh', - 'WC_Gateway_Paypal' - ) ); + 'WC_Gateway_Paypal', + ); - // Get order option - $ordering = (array) get_option('woocommerce_gateway_order'); - $order_end = 999; + /** + * Simplify Commerce is @deprecated in 2.6.0. Only load when enabled. + */ + if ( ! class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) && in_array( WC()->countries->get_base_country(), apply_filters( 'woocommerce_gateway_simplify_commerce_supported_countries', array( 'US', 'IE' ) ) ) ) { + $simplify_options = get_option( 'woocommerce_simplify_commerce_settings', array() ); + + if ( ! empty( $simplify_options['enabled'] ) && 'yes' === $simplify_options['enabled'] ) { + if ( function_exists( 'wcs_create_renewal_order' ) ) { + $load_gateways[] = 'WC_Addons_Gateway_Simplify_Commerce'; + } else { + $load_gateways[] = 'WC_Gateway_Simplify_Commerce'; + } + } + } + + // Filter + $load_gateways = apply_filters( 'woocommerce_payment_gateways', $load_gateways ); + + // Get sort order option + $ordering = (array) get_option( 'woocommerce_gateway_order' ); + $order_end = 999; // Load gateways in order - foreach ($load_gateways as $gateway) : + foreach ( $load_gateways as $gateway ) { + $load_gateway = is_string( $gateway ) ? new $gateway() : $gateway; - $load_gateway = new $gateway(); - - if (isset($ordering[$load_gateway->id]) && is_numeric($ordering[$load_gateway->id])) : + if ( isset( $ordering[ $load_gateway->id ] ) && is_numeric( $ordering[ $load_gateway->id ] ) ) { // Add in position - $this->payment_gateways[$ordering[$load_gateway->id]] = $load_gateway; - else : + $this->payment_gateways[ $ordering[ $load_gateway->id ] ] = $load_gateway; + } else { // Add to end of the array - $this->payment_gateways[$order_end] = $load_gateway; + $this->payment_gateways[ $order_end ] = $load_gateway; $order_end++; - endif; - - endforeach; + } + } ksort( $this->payment_gateways ); - } - - - /** - * Get gateways. - * - * @access public - * @return array - */ - function payment_gateways() { + } + /** + * Get gateways. + * @return array + */ + public function payment_gateways() { $_available_gateways = array(); - if ( sizeof( $this->payment_gateways ) > 0 ) - foreach ( $this->payment_gateways as $gateway ) + if ( sizeof( $this->payment_gateways ) > 0 ) { + foreach ( $this->payment_gateways as $gateway ) { $_available_gateways[ $gateway->id ] = $gateway; + } + } return $_available_gateways; } + /** + * Get array of registered gateway ids + * @since 2.6.0 + * @return array of strings + */ + public function get_payment_gateway_ids() { + return wp_list_pluck( $this->payment_gateways, 'id' ); + } /** * Get available gateways. * - * @access public * @return array */ - function get_available_payment_gateways() { - + public function get_available_payment_gateways() { $_available_gateways = array(); - foreach ( $this->payment_gateways as $gateway ) : - + foreach ( $this->payment_gateways as $gateway ) { if ( $gateway->is_available() ) { - if ( ! is_add_payment_method_page() ) - $_available_gateways[$gateway->id] = $gateway; - elseif( $gateway->supports( 'add_payment_method' ) ) - $_available_gateways[$gateway->id] = $gateway; + if ( ! is_add_payment_method_page() ) { + $_available_gateways[ $gateway->id ] = $gateway; + } elseif ( $gateway->supports( 'add_payment_method' ) ) { + $_available_gateways[ $gateway->id ] = $gateway; + } elseif ( $gateway->supports( 'tokenization' ) ) { + $_available_gateways[ $gateway->id ] = $gateway; + } } - - endforeach; + } return apply_filters( 'woocommerce_available_payment_gateways', $_available_gateways ); } + /** + * Set the current, active gateway. + * + * @param array $gateways Available payment gateways. + */ + public function set_current_gateway( $gateways ) { + // Be on the defensive + if ( ! is_array( $gateways ) || empty( $gateways ) ) { + return; + } + + if ( is_user_logged_in() ) { + $default_token = WC_Payment_Tokens::get_customer_default_token( get_current_user_id() ); + if ( ! is_null( $default_token ) ) { + $default_token_gateway = $default_token->get_gateway_id(); + } + } + + $current = ( isset( $default_token_gateway ) ? $default_token_gateway : WC()->session->get( 'chosen_payment_method' ) ); + + if ( $current && isset( $gateways[ $current ] ) ) { + $current_gateway = $gateways[ $current ]; + + } else { + $current_gateway = current( $gateways ); + } + + // Ensure we can make a call to set_current() without triggering an error + if ( $current_gateway && is_callable( array( $current_gateway, 'set_current' ) ) ) { + $current_gateway->set_current(); + } + } /** * Save options in admin. - * - * @access public - * @return void */ - function process_admin_options() { - - $default_gateway = ( isset( $_POST['default_gateway'] ) ) ? esc_attr( $_POST['default_gateway'] ) : ''; - $gateway_order = ( isset( $_POST['gateway_order'] ) ) ? $_POST['gateway_order'] : ''; - - $order = array(); + public function process_admin_options() { + $gateway_order = isset( $_POST['gateway_order'] ) ? $_POST['gateway_order'] : ''; + $order = array(); if ( is_array( $gateway_order ) && sizeof( $gateway_order ) > 0 ) { $loop = 0; @@ -168,7 +213,6 @@ class WC_Payment_Gateways { } } - update_option( 'woocommerce_default_gateway', $default_gateway ); update_option( 'woocommerce_gateway_order', $order ); } -} \ No newline at end of file +} diff --git a/includes/class-wc-payment-tokens.php b/includes/class-wc-payment-tokens.php new file mode 100644 index 00000000000..546c598b8cc --- /dev/null +++ b/includes/class-wc-payment-tokens.php @@ -0,0 +1,203 @@ + '', + 'user_id' => '', + 'gateway_id' => '', + 'type' => '', + ) ); + + $data_store = WC_Data_Store::load( 'payment-token' ); + $token_results = $data_store->get_tokens( $args ); + $tokens = array(); + + if ( ! empty( $token_results ) ) { + foreach ( $token_results as $token_result ) { + $_token = self::get( $token_result->token_id, $token_result ); + if ( ! empty( $_token ) ) { + $tokens[ $token_result->token_id ] = $_token; + } + } + } + + return $tokens; + } + + /** + * Returns an array of payment token objects associated with the passed customer ID. + * + * @since 2.6.0 + * @param int $customer_id Customer ID + * @param string $gateway_id Optional Gateway ID for getting tokens for a specific gateway + * @return array Array of token objects + */ + public static function get_customer_tokens( $customer_id, $gateway_id = '' ) { + if ( $customer_id < 1 ) { + return array(); + } + + $tokens = self::get_tokens( array( + 'user_id' => $customer_id, + 'gateway_id' => $gateway_id, + ) ); + + return apply_filters( 'woocommerce_get_customer_payment_tokens', $tokens, $customer_id, $gateway_id ); + } + + /** + * Returns a customers default token or NULL if there is no default token. + * + * @since 2.6.0 + * @param int $customer_id + * @return WC_Payment_Token|null + */ + public static function get_customer_default_token( $customer_id ) { + if ( $customer_id < 1 ) { + return null; + } + + $data_store = WC_Data_Store::load( 'payment-token' ); + $token = $data_store->get_users_default_token( $customer_id ); + + if ( $token ) { + return self::get( $token->token_id, $token ); + } else { + return null; + } + } + + /** + * Returns an array of payment token objects associated with the passed order ID. + * + * @since 2.6.0 + * @param int $order_id Order ID + * @return array Array of token objects + */ + public static function get_order_tokens( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return array(); + } + + $token_ids = $order->get_payment_tokens(); + + if ( empty( $token_ids ) ) { + return array(); + } + + $tokens = self::get_tokens( array( + 'token_id' => $token_ids, + ) ); + + return apply_filters( 'woocommerce_get_order_payment_tokens', $tokens, $order_id ); + } + + /** + * Get a token object by ID. + * + * @since 2.6.0 + * + * @param int $token_id Token ID + * @param object $token_result + * + * @return null|WC_Payment_Token Returns a valid payment token or null if no token can be found + */ + public static function get( $token_id, $token_result = null ) { + $data_store = WC_Data_Store::load( 'payment-token' ); + + if ( is_null( $token_result ) ) { + $token_result = $data_store->get_token_by_id( $token_id ); + // Still empty? Token doesn't exist? Don't continue + if ( empty( $token_result ) ) { + return null; + } + } + + $token_class = 'WC_Payment_Token_' . $token_result->type; + + if ( class_exists( $token_class ) ) { + $meta = $data_store->get_metadata( $token_id ); + $passed_meta = array(); + if ( ! empty( $meta ) ) { + foreach ( $meta as $meta_key => $meta_value ) { + $passed_meta[ $meta_key ] = $meta_value[0]; + } + } + return new $token_class( $token_id, (array) $token_result, $passed_meta ); + } + + return null; + } + + /** + * Remove a payment token from the database by ID. + * @since 2.6.0 + * @param WC_Payment_Token $token_id Token ID + */ + public static function delete( $token_id ) { + $type = self::get_token_type_by_id( $token_id ); + if ( ! empty( $type ) ) { + $class = 'WC_Payment_Token_' . $type; + $token = new $class( $token_id ); + $token->delete(); + } + } + + /** + * Loops through all of a users payment tokens and sets is_default to false for all but a specific token. + * + * @since 2.6.0 + * @param int $user_id User to set a default for + * @param int $token_id The ID of the token that should be default + */ + public static function set_users_default( $user_id, $token_id ) { + $data_store = WC_Data_Store::load( 'payment-token' ); + $users_tokens = self::get_customer_tokens( $user_id ); + foreach ( $users_tokens as $token ) { + if ( $token_id === $token->get_id() ) { + $data_store->set_default_status( $token->get_id(), true ); + do_action( 'woocommerce_payment_token_set_default', $token_id, $token ); + } else { + $data_store->set_default_status( $token->get_id(), false ); + } + } + } + + /** + * Returns what type (credit card, echeck, etc) of token a token is by ID. + * + * @since 2.6.0 + * @param int $token_id Token ID + * @return string Type + */ + public static function get_token_type_by_id( $token_id ) { + $data_store = WC_Data_Store::load( 'payment-token' ); + return $data_store->get_token_type_by_id( $token_id ); + } +} diff --git a/includes/class-wc-post-data.php b/includes/class-wc-post-data.php index 8593fb94105..9d44fa4fec1 100644 --- a/includes/class-wc-post-data.php +++ b/includes/class-wc-post-data.php @@ -5,7 +5,7 @@ if ( ! defined( 'ABSPATH' ) ) { } /** - * Post Data + * Post Data. * * Standardises certain post data on save. * @@ -17,28 +17,95 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_Post_Data { + /** + * Editing term. + * + * @var object + */ private static $editing_term = null; /** - * Hook in methods + * Hook in methods. */ public static function init() { + add_filter( 'post_type_link', array( __CLASS__, 'variation_post_link' ), 10, 2 ); + add_action( 'shutdown', array( __CLASS__, 'do_deferred_product_sync' ), 10 ); add_action( 'set_object_terms', array( __CLASS__, 'set_object_terms' ), 10, 6 ); add_action( 'transition_post_status', array( __CLASS__, 'transition_post_status' ), 10, 3 ); add_action( 'woocommerce_product_set_stock_status', array( __CLASS__, 'delete_product_query_transients' ) ); add_action( 'woocommerce_product_set_visibility', array( __CLASS__, 'delete_product_query_transients' ) ); + add_action( 'woocommerce_product_type_changed', array( __CLASS__, 'product_type_changed' ), 10, 3 ); add_action( 'edit_term', array( __CLASS__, 'edit_term' ), 10, 3 ); add_action( 'edited_term', array( __CLASS__, 'edited_term' ), 10, 3 ); add_filter( 'update_order_item_metadata', array( __CLASS__, 'update_order_item_metadata' ), 10, 5 ); add_filter( 'update_post_metadata', array( __CLASS__, 'update_post_metadata' ), 10, 5 ); add_filter( 'wp_insert_post_data', array( __CLASS__, 'wp_insert_post_data' ) ); - add_action( 'pre_post_update', array( __CLASS__, 'pre_post_update' ) ); + + // Status transitions + add_action( 'delete_post', array( __CLASS__, 'delete_post' ) ); + add_action( 'wp_trash_post', array( __CLASS__, 'trash_post' ) ); + add_action( 'untrashed_post', array( __CLASS__, 'untrash_post' ) ); + add_action( 'before_delete_post', array( __CLASS__, 'delete_order_items' ) ); + add_action( 'before_delete_post', array( __CLASS__, 'delete_order_downloadable_permissions' ) ); + + // Download permissions + add_action( 'woocommerce_process_product_file_download_paths', array( __CLASS__, 'process_product_file_download_paths' ), 10, 3 ); + + // Meta cache flushing. + add_action( 'updated_post_meta', array( __CLASS__, 'flush_object_meta_cache' ), 10, 4 ); + add_action( 'updated_order_item_meta', array( __CLASS__, 'flush_object_meta_cache' ), 10, 4 ); } /** - * Delete transients when terms are set + * Link to parent products when getting permalink for variation. + * + * @param string $permalink + * @param object $post + * + * @return string + */ + public static function variation_post_link( $permalink, $post ) { + if ( isset( $post->ID, $post->post_type ) && 'product_variation' === $post->post_type && ( $variation = wc_get_product( $post->ID ) ) ) { + return $variation->get_permalink(); + } + return $permalink; + } + + /** + * Sync products queued to sync. + */ + public static function do_deferred_product_sync() { + global $wc_deferred_product_sync; + + if ( ! empty( $wc_deferred_product_sync ) ) { + $wc_deferred_product_sync = wp_parse_id_list( $wc_deferred_product_sync ); + array_walk( $wc_deferred_product_sync, array( __CLASS__, 'deferred_product_sync' ) ); + } + } + + /** + * Sync a product. + * @param int $product_id + */ + public static function deferred_product_sync( $product_id ) { + $product = wc_get_product( $product_id ); + + if ( is_callable( array( $product, 'sync' ) ) ) { + $product->sync( $product ); + } + } + + /** + * Delete transients when terms are set. + * + * @param int $object_id + * @param mixed $terms + * @param array $tt_ids + * @param string $taxonomy + * @param mixed $append + * @param array $old_tt_ids */ public static function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { foreach ( array_merge( $tt_ids, $old_tt_ids ) as $id ) { @@ -47,7 +114,11 @@ class WC_Post_Data { } /** - * When a post status changes + * When a post status changes. + * + * @param string $new_status + * @param string $old_status + * @param object $post */ public static function transition_post_status( $new_status, $old_status, $post ) { if ( ( 'publish' === $new_status || 'publish' === $old_status ) && in_array( $post->post_type, array( 'product', 'product_variation' ) ) ) { @@ -67,8 +138,8 @@ class WC_Post_Data { global $wpdb; $wpdb->query( " - DELETE FROM `$wpdb->options` - WHERE `option_name` LIKE ('\_transient\_wc\_uf\_pid\_%') + DELETE FROM `$wpdb->options` + WHERE `option_name` LIKE ('\_transient\_wc\_uf\_pid\_%') OR `option_name` LIKE ('\_transient\_timeout\_wc\_uf\_pid\_%') OR `option_name` LIKE ('\_transient\_wc\_products\_will\_display\_%') OR `option_name` LIKE ('\_transient\_timeout\_wc\_products\_will\_display\_%') @@ -77,7 +148,23 @@ class WC_Post_Data { } /** - * When editing a term, check for product attributes + * Handle type changes. + * + * @since 3.0.0 + * @param WC_Product $product + * @param string $from + * @param string $to + */ + public static function product_type_changed( $product, $from, $to ) { + if ( 'variable' === $from && 'variable' !== $to ) { + // If the product is no longer variable, we should ensure all variations are removed. + $data_store = WC_Data_Store::load( 'product-variable' ); + $data_store->delete_variations( $product->get_id() ); + } + } + + /** + * When editing a term, check for product attributes. * @param id $term_id * @param id $tt_id * @param string $taxonomy @@ -91,7 +178,7 @@ class WC_Post_Data { } /** - * When a term is edited, check for product attributes and update variations + * When a term is edited, check for product attributes and update variations. * @param id $term_id * @param id $tt_id * @param string $taxonomy @@ -111,8 +198,8 @@ class WC_Post_Data { } /** - * Ensure floats are correctly converted to strings based on PHP locale - * + * Ensure floats are correctly converted to strings based on PHP locale. + * * @param null $check * @param int $object_id * @param string $meta_key @@ -136,8 +223,8 @@ class WC_Post_Data { } /** - * Ensure floats are correctly converted to strings based on PHP locale - * + * Ensure floats are correctly converted to strings based on PHP locale. + * * @param null $check * @param int $object_id * @param string $meta_key @@ -146,7 +233,12 @@ class WC_Post_Data { * @return null|bool */ public static function update_post_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) { - if ( ! empty( $meta_value ) && is_float( $meta_value ) && in_array( get_post_type( $object_id ), array( 'shop_order', 'shop_coupon', 'product', 'product_variation' ) ) ) { + // Delete product cache if someone uses meta directly. + if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ) ) ) { + wp_cache_delete( 'product-' . $object_id, 'products' ); + } + + if ( ! empty( $meta_value ) && is_float( $meta_value ) && in_array( get_post_type( $object_id ), array_merge( wc_get_order_types(), array( 'shop_coupon', 'product', 'product_variation' ) ) ) ) { // Convert float to string $meta_value = wc_float_to_string( $meta_value ); @@ -160,6 +252,16 @@ class WC_Post_Data { return $check; } + /** + * When setting stock level, ensure the stock status is kept in sync. + * @param int $meta_id + * @param int $object_id + * @param string $meta_key + * @param mixed $meta_value + * @deprecated + */ + public static function sync_product_stock_status( $meta_id, $object_id, $meta_key, $meta_value ) {} + /** * Forces the order posts to have a title in a certain format (containing the date). * Forces certain product data based on the product's type, e.g. grouped products cannot have a parent. @@ -171,12 +273,10 @@ class WC_Post_Data { if ( 'shop_order' === $data['post_type'] && isset( $data['post_date'] ) ) { $order_title = 'Order'; if ( $data['post_date'] ) { - $order_title.= ' – ' . date_i18n( 'F j, Y @ h:i A', strtotime( $data['post_date'] ) ); + $order_title .= ' – ' . date_i18n( 'F j, Y @ h:i A', strtotime( $data['post_date'] ) ); } $data['post_title'] = $order_title; - } - - elseif ( 'product' === $data['post_type'] && isset( $_POST['product-type'] ) ) { + } elseif ( 'product' === $data['post_type'] && isset( $_POST['product-type'] ) ) { $product_type = stripslashes( $_POST['product-type'] ); switch ( $product_type ) { case 'grouped' : @@ -184,26 +284,186 @@ class WC_Post_Data { $data['post_parent'] = 0; break; } + } elseif ( 'product' === $data['post_type'] && 'auto-draft' === $data['post_status'] ) { + $data['post_title'] = 'AUTO-DRAFT'; } return $data; - } + } /** - * Some functions, like the term recount, require the visibility to be set prior. Lets save that here. + * Removes variations etc belonging to a deleted post, and clears transients. * - * @param int $post_id + * @param mixed $id ID of post being deleted */ - public static function pre_post_update( $post_id ) { - if ( isset( $_POST['_visibility'] ) ) { - if ( update_post_meta( $post_id, '_visibility', wc_clean( $_POST['_visibility'] ) ) ) { - do_action( 'woocommerce_product_set_visibility', $post_id, wc_clean( $_POST['_visibility'] ) ); + public static function delete_post( $id ) { + if ( ! current_user_can( 'delete_posts' ) || ! $id ) { + return; + } + + $post_type = get_post_type( $id ); + + switch ( $post_type ) { + case 'product' : + $data_store = WC_Data_Store::load( 'product-variable' ); + $data_store->delete_variations( $id, true ); + + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + break; + case 'product_variation' : + wc_delete_product_transients( wp_get_post_parent_id( $id ) ); + break; + case 'shop_order' : + global $wpdb; + + $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); + + if ( ! is_null( $refunds ) ) { + foreach ( $refunds as $refund ) { + wp_delete_post( $refund->ID, true ); + } + } + break; + } + } + + /** + * woocommerce_trash_post function. + * + * @param mixed $id + */ + public static function trash_post( $id ) { + if ( ! $id ) { + return; + } + + $post_type = get_post_type( $id ); + + // If this is an order, trash any refunds too. + if ( in_array( $post_type, wc_get_order_types( 'order-count' ) ) ) { + global $wpdb; + + $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); + + foreach ( $refunds as $refund ) { + $wpdb->update( $wpdb->posts, array( 'post_status' => 'trash' ), array( 'ID' => $refund->ID ) ); + } + + wc_delete_shop_order_transients( $id ); + + // If this is a product, trash children variations. + } elseif ( 'product' === $post_type ) { + $data_store = WC_Data_Store::load( 'product-variable' ); + $data_store->delete_variations( $id, false ); + } + } + + /** + * woocommerce_untrash_post function. + * + * @param mixed $id + */ + public static function untrash_post( $id ) { + if ( ! $id ) { + return; + } + + $post_type = get_post_type( $id ); + + if ( in_array( $post_type, wc_get_order_types( 'order-count' ) ) ) { + global $wpdb; + + $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); + + foreach ( $refunds as $refund ) { + $wpdb->update( $wpdb->posts, array( 'post_status' => 'wc-completed' ), array( 'ID' => $refund->ID ) ); + } + + wc_delete_shop_order_transients( $id ); + + } elseif ( 'product' === $post_type ) { + $data_store = WC_Data_Store::load( 'product-variable' ); + $data_store->untrash_variations( $id ); + + wc_product_force_unique_sku( $id ); + } + } + + /** + * Remove item meta on permanent deletion. + * + * @param int $postid + */ + public static function delete_order_items( $postid ) { + global $wpdb; + + if ( in_array( get_post_type( $postid ), wc_get_order_types() ) ) { + do_action( 'woocommerce_delete_order_items', $postid ); + + $wpdb->query( " + DELETE {$wpdb->prefix}woocommerce_order_items, {$wpdb->prefix}woocommerce_order_itemmeta + FROM {$wpdb->prefix}woocommerce_order_items + JOIN {$wpdb->prefix}woocommerce_order_itemmeta ON {$wpdb->prefix}woocommerce_order_items.order_item_id = {$wpdb->prefix}woocommerce_order_itemmeta.order_item_id + WHERE {$wpdb->prefix}woocommerce_order_items.order_id = '{$postid}'; + " ); + + do_action( 'woocommerce_deleted_order_items', $postid ); + } + } + + /** + * Remove downloadable permissions on permanent order deletion. + * + * @param int $postid + */ + public static function delete_order_downloadable_permissions( $postid ) { + if ( in_array( get_post_type( $postid ), wc_get_order_types() ) ) { + do_action( 'woocommerce_delete_order_downloadable_permissions', $postid ); + + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->delete_by_order_id( $postid ); + + do_action( 'woocommerce_deleted_order_downloadable_permissions', $postid ); + } + } + + /** + * Update changed downloads. + * + * @param int $product_id product identifier + * @param int $variation_id optional product variation identifier + * @param array $downloads newly set files + */ + public static function process_product_file_download_paths( $product_id, $variation_id, $downloads ) { + if ( $variation_id ) { + $product_id = $variation_id; + } + $data_store = WC_Data_Store::load( 'customer-download' ); + + if ( $downloads ) { + foreach ( $downloads as $download ) { + $new_hash = md5( $download->get_file() ); + + if ( $download->get_previous_hash() && $download->get_previous_hash() !== $new_hash ) { + // Update permissions. + $data_store->update_download_id( $product_id, $download->get_previous_hash(), $new_hash ); + } } } - if ( isset( $_POST['_stock_status'] ) ) { - wc_update_product_stock_status( $post_id, wc_clean( $_POST['_stock_status'] ) ); - } - } + } + + /** + * Flush meta cache for CRUD objects on direct update. + * @param int $meta_id + * @param int $object_id + * @param string $meta_key + * @param string $meta_value + */ + public static function flush_object_meta_cache( $meta_id, $object_id, $meta_key, $meta_value ) { + WC_Cache_Helper::incr_cache_prefix( 'object_' . $object_id ); + } } -WC_Post_Data::init(); \ No newline at end of file +WC_Post_Data::init(); diff --git a/includes/class-wc-post-types.php b/includes/class-wc-post-types.php index bc7e56b61ea..3e8c0dc8577 100644 --- a/includes/class-wc-post-types.php +++ b/includes/class-wc-post-types.php @@ -1,413 +1,560 @@ false, - 'show_ui' => false, - 'show_in_nav_menus' => false, - 'query_var' => is_admin(), - 'rewrite' => false, - 'public' => false - ) ) - ); + apply_filters( 'woocommerce_taxonomy_objects_product_type', array( 'product' ) ), + apply_filters( 'woocommerce_taxonomy_args_product_type', array( + 'hierarchical' => false, + 'show_ui' => false, + 'show_in_nav_menus' => false, + 'query_var' => is_admin(), + 'rewrite' => false, + 'public' => false, + ) ) + ); + + register_taxonomy( 'product_visibility', + apply_filters( 'woocommerce_taxonomy_objects_product_visibility', array( 'product', 'product_variation' ) ), + apply_filters( 'woocommerce_taxonomy_args_product_visibility', array( + 'hierarchical' => false, + 'show_ui' => false, + 'show_in_nav_menus' => false, + 'query_var' => is_admin(), + 'rewrite' => false, + 'public' => false, + ) ) + ); register_taxonomy( 'product_cat', - apply_filters( 'woocommerce_taxonomy_objects_product_cat', array( 'product' ) ), - apply_filters( 'woocommerce_taxonomy_args_product_cat', array( - 'hierarchical' => true, - 'update_count_callback' => '_wc_term_recount', - 'label' => __( 'Product Categories', 'woocommerce' ), - 'labels' => array( - 'name' => __( 'Product Categories', 'woocommerce' ), - 'singular_name' => __( 'Product Category', 'woocommerce' ), - 'menu_name' => _x( 'Categories', 'Admin menu name', 'woocommerce' ), - 'search_items' => __( 'Search Product Categories', 'woocommerce' ), - 'all_items' => __( 'All Product Categories', 'woocommerce' ), - 'parent_item' => __( 'Parent Product Category', 'woocommerce' ), - 'parent_item_colon' => __( 'Parent Product Category:', 'woocommerce' ), - 'edit_item' => __( 'Edit Product Category', 'woocommerce' ), - 'update_item' => __( 'Update Product Category', 'woocommerce' ), - 'add_new_item' => __( 'Add New Product Category', 'woocommerce' ), - 'new_item_name' => __( 'New Product Category Name', 'woocommerce' ) - ), - 'show_ui' => true, - 'query_var' => true, - 'capabilities' => array( - 'manage_terms' => 'manage_product_terms', - 'edit_terms' => 'edit_product_terms', - 'delete_terms' => 'delete_product_terms', - 'assign_terms' => 'assign_product_terms', - ), - 'rewrite' => array( - 'slug' => empty( $permalinks['category_base'] ) ? _x( 'product-category', 'slug', 'woocommerce' ) : $permalinks['category_base'], + apply_filters( 'woocommerce_taxonomy_objects_product_cat', array( 'product' ) ), + apply_filters( 'woocommerce_taxonomy_args_product_cat', array( + 'hierarchical' => true, + 'update_count_callback' => '_wc_term_recount', + 'label' => __( 'Categories', 'woocommerce' ), + 'labels' => array( + 'name' => __( 'Product categories', 'woocommerce' ), + 'singular_name' => __( 'Category', 'woocommerce' ), + 'menu_name' => _x( 'Categories', 'Admin menu name', 'woocommerce' ), + 'search_items' => __( 'Search categories', 'woocommerce' ), + 'all_items' => __( 'All categories', 'woocommerce' ), + 'parent_item' => __( 'Parent category', 'woocommerce' ), + 'parent_item_colon' => __( 'Parent category:', 'woocommerce' ), + 'edit_item' => __( 'Edit category', 'woocommerce' ), + 'update_item' => __( 'Update category', 'woocommerce' ), + 'add_new_item' => __( 'Add new category', 'woocommerce' ), + 'new_item_name' => __( 'New category name', 'woocommerce' ), + 'not_found' => __( 'No categories found', 'woocommerce' ), + ), + 'show_ui' => true, + 'query_var' => true, + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', + ), + 'rewrite' => array( + 'slug' => $permalinks['category_rewrite_slug'], 'with_front' => false, 'hierarchical' => true, - ), - ) ) - ); - - register_taxonomy( 'product_tag', - apply_filters( 'woocommerce_taxonomy_objects_product_tag', array( 'product' ) ), - apply_filters( 'woocommerce_taxonomy_args_product_tag', array( - 'hierarchical' => false, - 'update_count_callback' => '_wc_term_recount', - 'label' => __( 'Product Tags', 'woocommerce' ), - 'labels' => array( - 'name' => __( 'Product Tags', 'woocommerce' ), - 'singular_name' => __( 'Product Tag', 'woocommerce' ), - 'menu_name' => _x( 'Tags', 'Admin menu name', 'woocommerce' ), - 'search_items' => __( 'Search Product Tags', 'woocommerce' ), - 'all_items' => __( 'All Product Tags', 'woocommerce' ), - 'parent_item' => __( 'Parent Product Tag', 'woocommerce' ), - 'parent_item_colon' => __( 'Parent Product Tag:', 'woocommerce' ), - 'edit_item' => __( 'Edit Product Tag', 'woocommerce' ), - 'update_item' => __( 'Update Product Tag', 'woocommerce' ), - 'add_new_item' => __( 'Add New Product Tag', 'woocommerce' ), - 'new_item_name' => __( 'New Product Tag Name', 'woocommerce' ) - ), - 'show_ui' => true, - 'query_var' => true, - 'capabilities' => array( - 'manage_terms' => 'manage_product_terms', - 'edit_terms' => 'edit_product_terms', - 'delete_terms' => 'delete_product_terms', - 'assign_terms' => 'assign_product_terms', ), - 'rewrite' => array( - 'slug' => empty( $permalinks['tag_base'] ) ? _x( 'product-tag', 'slug', 'woocommerce' ) : $permalinks['tag_base'], - 'with_front' => false - ), - ) ) - ); + ) ) + ); + + register_taxonomy( 'product_tag', + apply_filters( 'woocommerce_taxonomy_objects_product_tag', array( 'product' ) ), + apply_filters( 'woocommerce_taxonomy_args_product_tag', array( + 'hierarchical' => false, + 'update_count_callback' => '_wc_term_recount', + 'label' => __( 'Product tags', 'woocommerce' ), + 'labels' => array( + 'name' => __( 'Product tags', 'woocommerce' ), + 'singular_name' => __( 'Tag', 'woocommerce' ), + 'menu_name' => _x( 'Tags', 'Admin menu name', 'woocommerce' ), + 'search_items' => __( 'Search tags', 'woocommerce' ), + 'all_items' => __( 'All tags', 'woocommerce' ), + 'edit_item' => __( 'Edit tag', 'woocommerce' ), + 'update_item' => __( 'Update tag', 'woocommerce' ), + 'add_new_item' => __( 'Add new tag', 'woocommerce' ), + 'new_item_name' => __( 'New tag name', 'woocommerce' ), + 'popular_items' => __( 'Popular tags', 'woocommerce' ), + 'separate_items_with_commas' => __( 'Separate tags with commas', 'woocommerce' ), + 'add_or_remove_items' => __( 'Add or remove tags', 'woocommerce' ), + 'choose_from_most_used' => __( 'Choose from the most used tags', 'woocommerce' ), + 'not_found' => __( 'No tags found', 'woocommerce' ), + ), + 'show_ui' => true, + 'query_var' => true, + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', + ), + 'rewrite' => array( + 'slug' => $permalinks['tag_rewrite_slug'], + 'with_front' => false, + ), + ) ) + ); register_taxonomy( 'product_shipping_class', - apply_filters( 'woocommerce_taxonomy_objects_product_shipping_class', array('product', 'product_variation') ), - apply_filters( 'woocommerce_taxonomy_args_product_shipping_class', array( - 'hierarchical' => true, - 'update_count_callback' => '_update_post_term_count', - 'label' => __( 'Shipping Classes', 'woocommerce' ), - 'labels' => array( - 'name' => __( 'Shipping Classes', 'woocommerce' ), - 'singular_name' => __( 'Shipping Class', 'woocommerce' ), - 'menu_name' => _x( 'Shipping Classes', 'Admin menu name', 'woocommerce' ), - 'search_items' => __( 'Search Shipping Classes', 'woocommerce' ), - 'all_items' => __( 'All Shipping Classes', 'woocommerce' ), - 'parent_item' => __( 'Parent Shipping Class', 'woocommerce' ), - 'parent_item_colon' => __( 'Parent Shipping Class:', 'woocommerce' ), - 'edit_item' => __( 'Edit Shipping Class', 'woocommerce' ), - 'update_item' => __( 'Update Shipping Class', 'woocommerce' ), - 'add_new_item' => __( 'Add New Shipping Class', 'woocommerce' ), - 'new_item_name' => __( 'New Shipping Class Name', 'woocommerce' ) - ), - 'show_ui' => true, - 'show_in_nav_menus' => false, - 'query_var' => is_admin(), - 'capabilities' => array( - 'manage_terms' => 'manage_product_terms', - 'edit_terms' => 'edit_product_terms', - 'delete_terms' => 'delete_product_terms', - 'assign_terms' => 'assign_product_terms', + apply_filters( 'woocommerce_taxonomy_objects_product_shipping_class', array( 'product', 'product_variation' ) ), + apply_filters( 'woocommerce_taxonomy_args_product_shipping_class', array( + 'hierarchical' => false, + 'update_count_callback' => '_update_post_term_count', + 'label' => __( 'Shipping classes', 'woocommerce' ), + 'labels' => array( + 'name' => __( 'Product shipping classes', 'woocommerce' ), + 'singular_name' => __( 'Shipping class', 'woocommerce' ), + 'menu_name' => _x( 'Shipping classes', 'Admin menu name', 'woocommerce' ), + 'search_items' => __( 'Search shipping classes', 'woocommerce' ), + 'all_items' => __( 'All shipping classes', 'woocommerce' ), + 'parent_item' => __( 'Parent shipping class', 'woocommerce' ), + 'parent_item_colon' => __( 'Parent shipping class:', 'woocommerce' ), + 'edit_item' => __( 'Edit shipping class', 'woocommerce' ), + 'update_item' => __( 'Update shipping class', 'woocommerce' ), + 'add_new_item' => __( 'Add new shipping class', 'woocommerce' ), + 'new_item_name' => __( 'New shipping class Name', 'woocommerce' ), + ), + 'show_ui' => false, + 'show_in_quick_edit' => false, + 'show_in_nav_menus' => false, + 'query_var' => is_admin(), + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', ), - 'rewrite' => false, - ) ) - ); + 'rewrite' => false, + ) ) + ); - global $wc_product_attributes; + global $wc_product_attributes; - $wc_product_attributes = array(); + $wc_product_attributes = array(); if ( $attribute_taxonomies = wc_get_attribute_taxonomies() ) { foreach ( $attribute_taxonomies as $tax ) { - if ( $name = wc_attribute_taxonomy_name( $tax->attribute_name ) ) { + if ( $name = wc_attribute_taxonomy_name( $tax->attribute_name ) ) { + $tax->attribute_public = absint( isset( $tax->attribute_public ) ? $tax->attribute_public : 1 ); + $label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name; + $wc_product_attributes[ $name ] = $tax; + $taxonomy_data = array( + 'hierarchical' => false, + 'update_count_callback' => '_update_post_term_count', + 'labels' => array( + 'name' => sprintf( _x( 'Product %s', 'Product Attribute', 'woocommerce' ), $label ), + 'singular_name' => $label, + 'search_items' => sprintf( __( 'Search %s', 'woocommerce' ), $label ), + 'all_items' => sprintf( __( 'All %s', 'woocommerce' ), $label ), + 'parent_item' => sprintf( __( 'Parent %s', 'woocommerce' ), $label ), + 'parent_item_colon' => sprintf( __( 'Parent %s:', 'woocommerce' ), $label ), + 'edit_item' => sprintf( __( 'Edit %s', 'woocommerce' ), $label ), + 'update_item' => sprintf( __( 'Update %s', 'woocommerce' ), $label ), + 'add_new_item' => sprintf( __( 'Add new %s', 'woocommerce' ), $label ), + 'new_item_name' => sprintf( __( 'New %s', 'woocommerce' ), $label ), + 'not_found' => sprintf( __( 'No "%s" found', 'woocommerce' ), $label ), + ), + 'show_ui' => true, + 'show_in_quick_edit' => false, + 'show_in_menu' => false, + 'meta_box_cb' => false, + 'query_var' => 1 === $tax->attribute_public, + 'rewrite' => false, + 'sort' => false, + 'public' => 1 === $tax->attribute_public, + 'show_in_nav_menus' => 1 === $tax->attribute_public && apply_filters( 'woocommerce_attribute_show_in_nav_menus', false, $name ), + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', + ), + ); - $label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name; + if ( 1 === $tax->attribute_public && sanitize_title( $tax->attribute_name ) ) { + $taxonomy_data['rewrite'] = array( + 'slug' => trailingslashit( $permalinks['attribute_rewrite_slug'] ) . sanitize_title( $tax->attribute_name ), + 'with_front' => false, + 'hierarchical' => true, + ); + } - $wc_product_attributes[ $name ] = $tax; - - register_taxonomy( $name, - apply_filters( 'woocommerce_taxonomy_objects_' . $name, array( 'product' ) ), - apply_filters( 'woocommerce_taxonomy_args_' . $name, array( - 'hierarchical' => true, - 'update_count_callback' => '_update_post_term_count', - 'labels' => array( - 'name' => $label, - 'singular_name' => $label, - 'search_items' => sprintf( __( 'Search %s', 'woocommerce' ), $label ), - 'all_items' => sprintf( __( 'All %s', 'woocommerce' ), $label ), - 'parent_item' => sprintf( __( 'Parent %s', 'woocommerce' ), $label ), - 'parent_item_colon' => sprintf( __( 'Parent %s:', 'woocommerce' ), $label ), - 'edit_item' => sprintf( __( 'Edit %s', 'woocommerce' ), $label ), - 'update_item' => sprintf( __( 'Update %s', 'woocommerce' ), $label ), - 'add_new_item' => sprintf( __( 'Add New %s', 'woocommerce' ), $label ), - 'new_item_name' => sprintf( __( 'New %s', 'woocommerce' ), $label ) - ), - 'show_ui' => false, - 'query_var' => true, - 'capabilities' => array( - 'manage_terms' => 'manage_product_terms', - 'edit_terms' => 'edit_product_terms', - 'delete_terms' => 'delete_product_terms', - 'assign_terms' => 'assign_product_terms', - ), - 'show_in_nav_menus' => apply_filters( 'woocommerce_attribute_show_in_nav_menus', false, $name ), - 'rewrite' => array( - 'slug' => ( empty( $permalinks['attribute_base'] ) ? '' : trailingslashit( $permalinks['attribute_base'] ) ) . sanitize_title( $tax->attribute_name ), - 'with_front' => false, - 'hierarchical' => true - ), - ) ) - ); - } - } - - do_action( 'woocommerce_after_register_taxonomy' ); + register_taxonomy( $name, apply_filters( "woocommerce_taxonomy_objects_{$name}", array( 'product' ) ), apply_filters( "woocommerce_taxonomy_args_{$name}", $taxonomy_data ) ); + } + } } + + do_action( 'woocommerce_after_register_taxonomy' ); } /** - * Register core post types + * Register core post types. */ public static function register_post_types() { - if ( post_type_exists('product') ) + if ( ! is_blog_installed() || post_type_exists( 'product' ) ) { return; + } do_action( 'woocommerce_register_post_type' ); - $permalinks = get_option( 'woocommerce_permalinks' ); - $product_permalink = empty( $permalinks['product_base'] ) ? _x( 'product', 'slug', 'woocommerce' ) : $permalinks['product_base']; + $permalinks = wc_get_permalink_structure(); + $supports = array( 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields', 'publicize', 'wpcom-markdown' ); - register_post_type( "product", + if ( 'yes' === get_option( 'woocommerce_enable_reviews', 'yes' ) ) { + $supports[] = 'comments'; + } + + register_post_type( 'product', apply_filters( 'woocommerce_register_post_type_product', array( - 'labels' => array( - 'name' => __( 'Products', 'woocommerce' ), - 'singular_name' => __( 'Product', 'woocommerce' ), - 'menu_name' => _x( 'Products', 'Admin menu name', 'woocommerce' ), - 'add_new' => __( 'Add Product', 'woocommerce' ), - 'add_new_item' => __( 'Add New Product', 'woocommerce' ), - 'edit' => __( 'Edit', 'woocommerce' ), - 'edit_item' => __( 'Edit Product', 'woocommerce' ), - 'new_item' => __( 'New Product', 'woocommerce' ), - 'view' => __( 'View Product', 'woocommerce' ), - 'view_item' => __( 'View Product', 'woocommerce' ), - 'search_items' => __( 'Search Products', 'woocommerce' ), - 'not_found' => __( 'No Products found', 'woocommerce' ), - 'not_found_in_trash' => __( 'No Products found in trash', 'woocommerce' ), - 'parent' => __( 'Parent Product', 'woocommerce' ) + 'labels' => array( + 'name' => __( 'Products', 'woocommerce' ), + 'singular_name' => __( 'Product', 'woocommerce' ), + 'all_items' => __( 'All Products', 'woocommerce' ), + 'menu_name' => _x( 'Products', 'Admin menu name', 'woocommerce' ), + 'add_new' => __( 'Add New', 'woocommerce' ), + 'add_new_item' => __( 'Add new product', 'woocommerce' ), + 'edit' => __( 'Edit', 'woocommerce' ), + 'edit_item' => __( 'Edit product', 'woocommerce' ), + 'new_item' => __( 'New product', 'woocommerce' ), + 'view' => __( 'View product', 'woocommerce' ), + 'view_item' => __( 'View product', 'woocommerce' ), + 'search_items' => __( 'Search products', 'woocommerce' ), + 'not_found' => __( 'No products found', 'woocommerce' ), + 'not_found_in_trash' => __( 'No products found in trash', 'woocommerce' ), + 'parent' => __( 'Parent product', 'woocommerce' ), + 'featured_image' => __( 'Product image', 'woocommerce' ), + 'set_featured_image' => __( 'Set product image', 'woocommerce' ), + 'remove_featured_image' => __( 'Remove product image', 'woocommerce' ), + 'use_featured_image' => __( 'Use as product image', 'woocommerce' ), + 'insert_into_item' => __( 'Insert into product', 'woocommerce' ), + 'uploaded_to_this_item' => __( 'Uploaded to this product', 'woocommerce' ), + 'filter_items_list' => __( 'Filter products', 'woocommerce' ), + 'items_list_navigation' => __( 'Products navigation', 'woocommerce' ), + 'items_list' => __( 'Products list', 'woocommerce' ), ), - 'description' => __( 'This is where you can add new products to your store.', 'woocommerce' ), - 'public' => true, - 'show_ui' => true, - 'capability_type' => 'product', - 'map_meta_cap' => true, - 'publicly_queryable' => true, - 'exclude_from_search' => false, - 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! - 'rewrite' => $product_permalink ? array( 'slug' => untrailingslashit( $product_permalink ), 'with_front' => false, 'feeds' => true ) : false, - 'query_var' => true, - 'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'comments', 'custom-fields', 'page-attributes' ), - 'has_archive' => ( $shop_page_id = wc_get_page_id( 'shop' ) ) && get_post( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop', - 'show_in_nav_menus' => true + 'description' => __( 'This is where you can add new products to your store.', 'woocommerce' ), + 'public' => true, + 'show_ui' => true, + 'capability_type' => 'product', + 'map_meta_cap' => true, + 'publicly_queryable' => true, + 'exclude_from_search' => false, + 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! + 'rewrite' => $permalinks['product_rewrite_slug'] ? array( 'slug' => $permalinks['product_rewrite_slug'], 'with_front' => false, 'feeds' => true ) : false, + 'query_var' => true, + 'supports' => $supports, + 'has_archive' => ( $shop_page_id = wc_get_page_id( 'shop' ) ) && get_post( $shop_page_id ) ? urldecode( get_page_uri( $shop_page_id ) ) : 'shop', + 'show_in_nav_menus' => true, + 'show_in_rest' => true, ) ) ); - if ( preg_match( '/\/(.+)(\/%product_cat%)$/' , $product_permalink, $matches ) ) { - add_rewrite_rule( '^' . $matches[1] . '/.+?/[^/]+/([^/]+)/?$', 'index.php?attachment=$matches[1]', 'top' ); - } - - register_post_type( "product_variation", + register_post_type( 'product_variation', apply_filters( 'woocommerce_register_post_type_product_variation', array( - 'label' => __( 'Variations', 'woocommerce' ), - 'public' => false, - 'hierarchical' => false, - 'supports' => false + 'label' => __( 'Variations', 'woocommerce' ), + 'public' => false, + 'hierarchical' => false, + 'supports' => false, + 'capability_type' => 'product', + 'rewrite' => false, ) ) ); - $menu_name = _x('Orders', 'Admin menu name', 'woocommerce' ); - - if ( $order_count = wc_processing_order_count() ) { - $menu_name .= " " . number_format_i18n( $order_count ) . "" ; - } - - register_post_type( "shop_order", - apply_filters( 'woocommerce_register_post_type_shop_order', + wc_register_order_type( + 'shop_order', + apply_filters( 'woocommerce_register_post_type_shop_order', array( - 'labels' => array( - 'name' => __( 'Orders', 'woocommerce' ), - 'singular_name' => __( 'Order', 'woocommerce' ), - 'add_new' => __( 'Add Order', 'woocommerce' ), - 'add_new_item' => __( 'Add New Order', 'woocommerce' ), - 'edit' => __( 'Edit', 'woocommerce' ), - 'edit_item' => __( 'Edit Order', 'woocommerce' ), - 'new_item' => __( 'New Order', 'woocommerce' ), - 'view' => __( 'View Order', 'woocommerce' ), - 'view_item' => __( 'View Order', 'woocommerce' ), - 'search_items' => __( 'Search Orders', 'woocommerce' ), - 'not_found' => __( 'No Orders found', 'woocommerce' ), - 'not_found_in_trash' => __( 'No Orders found in trash', 'woocommerce' ), - 'parent' => __( 'Parent Orders', 'woocommerce' ), - 'menu_name' => $menu_name + 'labels' => array( + 'name' => __( 'Orders', 'woocommerce' ), + 'singular_name' => _x( 'Order', 'shop_order post type singular name', 'woocommerce' ), + 'add_new' => __( 'Add order', 'woocommerce' ), + 'add_new_item' => __( 'Add new order', 'woocommerce' ), + 'edit' => __( 'Edit', 'woocommerce' ), + 'edit_item' => __( 'Edit order', 'woocommerce' ), + 'new_item' => __( 'New order', 'woocommerce' ), + 'view' => __( 'View order', 'woocommerce' ), + 'view_item' => __( 'View order', 'woocommerce' ), + 'search_items' => __( 'Search orders', 'woocommerce' ), + 'not_found' => __( 'No orders found', 'woocommerce' ), + 'not_found_in_trash' => __( 'No orders found in trash', 'woocommerce' ), + 'parent' => __( 'Parent orders', 'woocommerce' ), + 'menu_name' => _x( 'Orders', 'Admin menu name', 'woocommerce' ), + 'filter_items_list' => __( 'Filter orders', 'woocommerce' ), + 'items_list_navigation' => __( 'Orders navigation', 'woocommerce' ), + 'items_list' => __( 'Orders list', 'woocommerce' ), ), - 'description' => __( 'This is where store orders are stored.', 'woocommerce' ), - 'public' => false, - 'show_ui' => true, - 'capability_type' => 'shop_order', - 'map_meta_cap' => true, - 'publicly_queryable' => false, - 'exclude_from_search' => true, - 'show_in_menu' => current_user_can( 'manage_woocommerce' ) ? 'woocommerce' : true, - 'hierarchical' => false, - 'show_in_nav_menus' => false, - 'rewrite' => false, - 'query_var' => false, - 'supports' => array( 'title', 'comments', 'custom-fields' ), - 'has_archive' => false, + 'description' => __( 'This is where store orders are stored.', 'woocommerce' ), + 'public' => false, + 'show_ui' => true, + 'capability_type' => 'shop_order', + 'map_meta_cap' => true, + 'publicly_queryable' => false, + 'exclude_from_search' => true, + 'show_in_menu' => current_user_can( 'manage_woocommerce' ) ? 'woocommerce' : true, + 'hierarchical' => false, + 'show_in_nav_menus' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'comments', 'custom-fields' ), + 'has_archive' => false, ) ) ); - if ( get_option( 'woocommerce_enable_coupons' ) == 'yes' ) { - register_post_type( "shop_coupon", - apply_filters( 'woocommerce_register_post_type_shop_coupon', + wc_register_order_type( + 'shop_order_refund', + apply_filters( 'woocommerce_register_post_type_shop_order_refund', + array( + 'label' => __( 'Refunds', 'woocommerce' ), + 'capability_type' => 'shop_order', + 'public' => false, + 'hierarchical' => false, + 'supports' => false, + 'exclude_from_orders_screen' => false, + 'add_order_meta_boxes' => false, + 'exclude_from_order_count' => true, + 'exclude_from_order_views' => false, + 'exclude_from_order_reports' => false, + 'exclude_from_order_sales_reports' => true, + 'class_name' => 'WC_Order_Refund', + 'rewrite' => false, + ) + ) + ); + + if ( 'yes' == get_option( 'woocommerce_enable_coupons' ) ) { + register_post_type( 'shop_coupon', + apply_filters( 'woocommerce_register_post_type_shop_coupon', array( - 'labels' => array( - 'name' => __( 'Coupons', 'woocommerce' ), - 'singular_name' => __( 'Coupon', 'woocommerce' ), - 'menu_name' => _x( 'Coupons', 'Admin menu name', 'woocommerce' ), - 'add_new' => __( 'Add Coupon', 'woocommerce' ), - 'add_new_item' => __( 'Add New Coupon', 'woocommerce' ), - 'edit' => __( 'Edit', 'woocommerce' ), - 'edit_item' => __( 'Edit Coupon', 'woocommerce' ), - 'new_item' => __( 'New Coupon', 'woocommerce' ), - 'view' => __( 'View Coupons', 'woocommerce' ), - 'view_item' => __( 'View Coupon', 'woocommerce' ), - 'search_items' => __( 'Search Coupons', 'woocommerce' ), - 'not_found' => __( 'No Coupons found', 'woocommerce' ), - 'not_found_in_trash' => __( 'No Coupons found in trash', 'woocommerce' ), - 'parent' => __( 'Parent Coupon', 'woocommerce' ) + 'labels' => array( + 'name' => __( 'Coupons', 'woocommerce' ), + 'singular_name' => __( 'Coupon', 'woocommerce' ), + 'menu_name' => _x( 'Coupons', 'Admin menu name', 'woocommerce' ), + 'add_new' => __( 'Add coupon', 'woocommerce' ), + 'add_new_item' => __( 'Add new coupon', 'woocommerce' ), + 'edit' => __( 'Edit', 'woocommerce' ), + 'edit_item' => __( 'Edit coupon', 'woocommerce' ), + 'new_item' => __( 'New coupon', 'woocommerce' ), + 'view' => __( 'View coupons', 'woocommerce' ), + 'view_item' => __( 'View coupon', 'woocommerce' ), + 'search_items' => __( 'Search coupons', 'woocommerce' ), + 'not_found' => __( 'No coupons found', 'woocommerce' ), + 'not_found_in_trash' => __( 'No coupons found in trash', 'woocommerce' ), + 'parent' => __( 'Parent coupon', 'woocommerce' ), + 'filter_items_list' => __( 'Filter coupons', 'woocommerce' ), + 'items_list_navigation' => __( 'Coupons navigation', 'woocommerce' ), + 'items_list' => __( 'Coupons list', 'woocommerce' ), ), - 'description' => __( 'This is where you can add new coupons that customers can use in your store.', 'woocommerce' ), - 'public' => false, - 'show_ui' => true, - 'capability_type' => 'shop_coupon', - 'map_meta_cap' => true, - 'publicly_queryable' => false, - 'exclude_from_search' => true, - 'show_in_menu' => current_user_can( 'manage_woocommerce' ) ? 'woocommerce' : true, - 'hierarchical' => false, - 'rewrite' => false, - 'query_var' => false, - 'supports' => array( 'title' ), - 'show_in_nav_menus' => false, - 'show_in_admin_bar' => true + 'description' => __( 'This is where you can add new coupons that customers can use in your store.', 'woocommerce' ), + 'public' => false, + 'show_ui' => true, + 'capability_type' => 'shop_coupon', + 'map_meta_cap' => true, + 'publicly_queryable' => false, + 'exclude_from_search' => true, + 'show_in_menu' => current_user_can( 'manage_woocommerce' ) ? 'woocommerce' : true, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title' ), + 'show_in_nav_menus' => false, + 'show_in_admin_bar' => true, ) ) ); } + + register_post_type( 'shop_webhook', + apply_filters( 'woocommerce_register_post_type_shop_webhook', + array( + 'labels' => array( + 'name' => __( 'Webhooks', 'woocommerce' ), + 'singular_name' => __( 'Webhook', 'woocommerce' ), + 'menu_name' => _x( 'Webhooks', 'Admin menu name', 'woocommerce' ), + 'add_new' => __( 'Add webhook', 'woocommerce' ), + 'add_new_item' => __( 'Add new webhook', 'woocommerce' ), + 'edit' => __( 'Edit', 'woocommerce' ), + 'edit_item' => __( 'Edit webhook', 'woocommerce' ), + 'new_item' => __( 'New webhook', 'woocommerce' ), + 'view' => __( 'View webhooks', 'woocommerce' ), + 'view_item' => __( 'View webhook', 'woocommerce' ), + 'search_items' => __( 'Search webhooks', 'woocommerce' ), + 'not_found' => __( 'No webhooks found', 'woocommerce' ), + 'not_found_in_trash' => __( 'No webhooks found in trash', 'woocommerce' ), + 'parent' => __( 'Parent webhook', 'woocommerce' ), + ), + 'public' => false, + 'show_ui' => true, + 'capability_type' => 'shop_webhook', + 'map_meta_cap' => true, + 'publicly_queryable' => false, + 'exclude_from_search' => true, + 'show_in_menu' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => false, + 'show_in_nav_menus' => false, + 'show_in_admin_bar' => false, + ) + ) + ); + + do_action( 'woocommerce_after_register_post_type' ); } /** - * Register our custom post statuses, used for order status + * Register our custom post statuses, used for order status. */ public static function register_post_status() { - register_post_status( 'wc-pending', array( - 'label' => _x( 'Pending payment', 'Order status', 'woocommerce' ), - 'public' => true, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - 'label_count' => _n_noop( 'Pending payment (%s)', 'Pending payment (%s)', 'woocommerce' ) - ) ); - register_post_status( 'wc-processing', array( - 'label' => _x( 'Processing', 'Order status', 'woocommerce' ), - 'public' => true, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - 'label_count' => _n_noop( 'Processing (%s)', 'Processing (%s)', 'woocommerce' ) - ) ); - register_post_status( 'wc-on-hold', array( - 'label' => _x( 'On hold', 'Order status', 'woocommerce' ), - 'public' => true, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - 'label_count' => _n_noop( 'On hold (%s)', 'On hold (%s)', 'woocommerce' ) - ) ); - register_post_status( 'wc-completed', array( - 'label' => _x( 'Completed', 'Order status', 'woocommerce' ), - 'public' => true, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - 'label_count' => _n_noop( 'Completed (%s)', 'Completed (%s)', 'woocommerce' ) - ) ); - register_post_status( 'wc-cancelled', array( - 'label' => _x( 'Cancelled', 'Order status', 'woocommerce' ), - 'public' => true, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - 'label_count' => _n_noop( 'Cancelled (%s)', 'Cancelled (%s)', 'woocommerce' ) - ) ); - register_post_status( 'wc-refunded', array( - 'label' => _x( 'Refunded', 'Order status', 'woocommerce' ), - 'public' => true, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - 'label_count' => _n_noop( 'Refunded (%s)', 'Refunded (%s)', 'woocommerce' ) - ) ); - register_post_status( 'wc-failed', array( - 'label' => _x( 'Failed', 'Order status', 'woocommerce' ), - 'public' => true, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - 'label_count' => _n_noop( 'Failed (%s)', 'Failed (%s)', 'woocommerce' ) - ) ); + + $order_statuses = apply_filters( 'woocommerce_register_shop_order_post_statuses', + array( + 'wc-pending' => array( + 'label' => _x( 'Pending payment', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Pending payment (%s)', 'Pending payment (%s)', 'woocommerce' ), + ), + 'wc-processing' => array( + 'label' => _x( 'Processing', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Processing (%s)', 'Processing (%s)', 'woocommerce' ), + ), + 'wc-on-hold' => array( + 'label' => _x( 'On hold', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'On hold (%s)', 'On hold (%s)', 'woocommerce' ), + ), + 'wc-completed' => array( + 'label' => _x( 'Completed', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Completed (%s)', 'Completed (%s)', 'woocommerce' ), + ), + 'wc-cancelled' => array( + 'label' => _x( 'Cancelled', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Cancelled (%s)', 'Cancelled (%s)', 'woocommerce' ), + ), + 'wc-refunded' => array( + 'label' => _x( 'Refunded', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Refunded (%s)', 'Refunded (%s)', 'woocommerce' ), + ), + 'wc-failed' => array( + 'label' => _x( 'Failed', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Failed (%s)', 'Failed (%s)', 'woocommerce' ), + ), + ) + ); + + foreach ( $order_statuses as $order_status => $values ) { + register_post_status( $order_status, $values ); + } + } + + /** + * Flush rewrite rules. + */ + public static function flush_rewrite_rules() { + flush_rewrite_rules(); + } + + /** + * Add Product Support to Jetpack Omnisearch. + */ + public static function support_jetpack_omnisearch() { + if ( class_exists( 'Jetpack_Omnisearch_Posts' ) ) { + new Jetpack_Omnisearch_Posts( 'product' ); + } + } + + /** + * Added product for Jetpack related posts. + * + * @param array $post_types + * @return array + */ + public static function rest_api_allowed_post_types( $post_types ) { + $post_types[] = 'product'; + + return $post_types; } } diff --git a/includes/class-wc-product-attribute.php b/includes/class-wc-product-attribute.php new file mode 100644 index 00000000000..4dc117dede3 --- /dev/null +++ b/includes/class-wc-product-attribute.php @@ -0,0 +1,329 @@ + 0, + 'name' => '', + 'options' => array(), + 'position' => 0, + 'visible' => false, + 'variation' => false, + ); + + /** + * Return if this attribute is a taxonomy. + * + * @return boolean + */ + public function is_taxonomy() { + return 0 < $this->get_id(); + } + + /** + * Get taxonomy name if applicable. + * + * @return string + */ + public function get_taxonomy() { + return $this->is_taxonomy() ? $this->get_name() : ''; + } + + /** + * Get taxonomy object. + * + * @return array|null + */ + public function get_taxonomy_object() { + global $wc_product_attributes; + return $this->is_taxonomy() ? $wc_product_attributes[ $this->get_name() ] : null; + } + + /** + * Gets terms from the stored options. + * + * @return array|null + */ + public function get_terms() { + if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) { + return null; + } + $terms = array(); + foreach ( $this->get_options() as $option ) { + if ( is_int( $option ) ) { + $term = get_term_by( 'id', $option, $this->get_name() ); + } else { + // Term names get escaped in WP. See sanitize_term_field. + $term = get_term_by( 'name', $option, $this->get_name() ); + + if ( ! $term || is_wp_error( $term ) ) { + $new_term = wp_insert_term( $option, $this->get_name() ); + $term = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() ); + } + } + if ( $term && ! is_wp_error( $term ) ) { + $terms[] = $term; + } + } + return $terms; + } + + /** + * Gets slugs from the stored options, or just the string if text based. + * + * @return array + */ + public function get_slugs() { + if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) { + return $this->get_options(); + } + $terms = array(); + foreach ( $this->get_options() as $option ) { + if ( is_int( $option ) ) { + $term = get_term_by( 'id', $option, $this->get_name() ); + } else { + $term = get_term_by( 'name', $option, $this->get_name() ); + + if ( ! $term || is_wp_error( $term ) ) { + $new_term = wp_insert_term( $option, $this->get_name() ); + $term = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() ); + } + } + if ( $term && ! is_wp_error( $term ) ) { + $terms[] = $term->slug; + } + } + return $terms; + } + + /** + * Returns all data for this object. + * + * @return array + */ + public function get_data() { + return array_merge( $this->data, array( + 'is_visible' => $this->get_visible() ? 1 : 0, + 'is_variation' => $this->get_variation() ? 1 : 0, + 'is_taxonomy' => $this->is_taxonomy() ? 1 : 0, + 'value' => $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ), + ) ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set ID (this is the attribute ID). + * + * @param int $value + */ + public function set_id( $value ) { + $this->data['id'] = absint( $value ); + } + + /** + * Set name (this is the attribute name or taxonomy). + * + * @param int $value + */ + public function set_name( $value ) { + $this->data['name'] = $value; + } + + /** + * Set options. + * + * @param array $value + */ + public function set_options( $value ) { + $this->data['options'] = $value; + } + + /** + * Set position. + * + * @param int $value + */ + public function set_position( $value ) { + $this->data['position'] = absint( $value ); + } + + /** + * Set if visible. + * + * @param bool $value + */ + public function set_visible( $value ) { + $this->data['visible'] = wc_string_to_bool( $value ); + } + + /** + * Set if variation. + * + * @param bool $value + */ + public function set_variation( $value ) { + $this->data['variation'] = wc_string_to_bool( $value ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get the ID. + * + * @return int + */ + public function get_id() { + return $this->data['id']; + } + + /** + * Get name. + * + * @return int + */ + public function get_name() { + return $this->data['name']; + } + + /** + * Get options. + * + * @return array + */ + public function get_options() { + return $this->data['options']; + } + + /** + * Get position. + * + * @return int + */ + public function get_position() { + return $this->data['position']; + } + + /** + * Get if visible. + * + * @return bool + */ + public function get_visible() { + return $this->data['visible']; + } + + /** + * Get if variation. + * + * @return bool + */ + public function get_variation() { + return $this->data['variation']; + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * offsetGet. + * + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + switch ( $offset ) { + case 'is_variation' : + return $this->get_variation() ? 1 : 0; + break; + case 'is_visible' : + return $this->get_visible() ? 1 : 0; + break; + case 'is_taxonomy' : + return $this->is_taxonomy() ? 1 : 0; + break; + case 'value' : + return $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ); + break; + default : + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + break; + } + return ''; + } + + /** + * offsetSet. + * + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + switch ( $offset ) { + case 'is_variation' : + $this->set_variation( $value ); + break; + case 'is_visible' : + $this->set_visible( $value ); + break; + case 'value' : + $this->set_options( $value ); + break; + default : + if ( is_callable( array( $this, "set_$offset" ) ) ) { + return $this->{"set_$offset"}( $value ); + } + break; + } + } + + /** + * offsetUnset. + * + * @param string $offset + */ + public function offsetUnset( $offset ) {} + + /** + * offsetExists. + * + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + return in_array( $offset, array_merge( array( 'is_variation', 'is_visible', 'is_taxonomy', 'value' ), array_keys( $this->data ) ) ); + } +} diff --git a/includes/class-wc-product-download.php b/includes/class-wc-product-download.php new file mode 100644 index 00000000000..63e2744bd6e --- /dev/null +++ b/includes/class-wc-product-download.php @@ -0,0 +1,240 @@ + '', + 'name' => '', + 'file' => '', + 'previous_hash' => '', + ); + + /** + * Returns all data for this object. + * @return array + */ + public function get_data() { + return $this->data; + } + + /** + * Get allowed mime types. + * @return array + */ + public function get_allowed_mime_types() { + return apply_filters( 'woocommerce_downloadable_file_allowed_mime_types', get_allowed_mime_types() ); + } + + /** + * Get type of file path set. + * @param string $file_path optional. + * @return string absolute, relative, or shortcode. + */ + public function get_type_of_file_path( $file_path = '' ) { + $file_path = $file_path ? $file_path : $this->get_file(); + if ( 0 === strpos( $file_path, 'http' ) ) { + return 'absolute'; + } elseif ( '[' === substr( $file_path, 0, 1 ) && ']' === substr( $file_path, -1 ) ) { + return 'shortcode'; + } else { + return 'relative'; + } + } + + /** + * Get file type. + * @return string + */ + public function get_file_type() { + $type = wp_check_filetype( strtok( $this->get_file(), '?' ), $this->get_allowed_mime_types() ); + return $type['type']; + } + + /** + * Get file extension. + * @return string + */ + public function get_file_extension() { + $parsed_url = parse_url( $this->get_file(), PHP_URL_PATH ); + return pathinfo( $parsed_url, PATHINFO_EXTENSION ); + } + + /** + * Check if file is allowed. + * @return boolean + */ + public function is_allowed_filetype() { + if ( 'relative' !== $this->get_type_of_file_path() ) { + return true; + } + return ! $this->get_file_extension() || in_array( $this->get_file_type(), $this->get_allowed_mime_types() ); + } + + /** + * Validate file exists. + * @return boolean + */ + public function file_exists() { + if ( 'relative' !== $this->get_type_of_file_path() ) { + return true; + } + $file_url = $this->get_file(); + if ( '..' === substr( $file_url, 0, 2 ) || '/' !== substr( $file_url, 0, 1 ) ) { + $file_url = realpath( ABSPATH . $file_url ); + } elseif ( '/wp-content' === substr( $file_url, 0, 11 ) ) { + $file_url = realpath( WP_CONTENT_DIR . substr( $file_url, 11 ) ); + } + return apply_filters( 'woocommerce_downloadable_file_exists', file_exists( $file_url ), $this->get_file() ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set ID. + * @param string $value + */ + public function set_id( $value ) { + $this->data['id'] = wc_clean( $value ); + } + + /** + * Set name. + * @param string $value + */ + public function set_name( $value ) { + $this->data['name'] = wc_clean( $value ); + } + + /** + * Set previous_hash. + * @param string $value + */ + public function set_previous_hash( $value ) { + $this->data['previous_hash'] = wc_clean( $value ); + } + + /** + * Set file. + * @param string $value + */ + public function set_file( $value ) { + switch ( $this->get_type_of_file_path( $value ) ) { + case 'absolute' : + $this->data['file'] = esc_url_raw( $value ); + break; + default: + $this->data['file'] = wc_clean( $value ); + break; + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get id. + * @return string + */ + public function get_id() { + return $this->data['id']; + } + + /** + * Get name. + * @return string + */ + public function get_name() { + return $this->data['name']; + } + + /** + * Get previous_hash. + * @return string + */ + public function get_previous_hash() { + return $this->data['previous_hash']; + } + + /** + * Get file. + * @return string + */ + public function get_file() { + return $this->data['file']; + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * offsetGet + * @param string $offset + * @return mixed + */ + public function offsetGet( $offset ) { + switch ( $offset ) { + default : + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + break; + } + return ''; + } + + /** + * offsetSet + * @param string $offset + * @param mixed $value + */ + public function offsetSet( $offset, $value ) { + switch ( $offset ) { + default : + if ( is_callable( array( $this, "set_$offset" ) ) ) { + return $this->{"set_$offset"}( $value ); + } + break; + } + } + + /** + * offsetUnset + * @param string $offset + */ + public function offsetUnset( $offset ) {} + + /** + * offsetExists + * @param string $offset + * @return bool + */ + public function offsetExists( $offset ) { + return in_array( $offset, array_keys( $this->data ) ); + } +} diff --git a/includes/class-wc-product-external.php b/includes/class-wc-product-external.php index 3b5423fe398..0508a4f461a 100644 --- a/includes/class-wc-product-external.php +++ b/includes/class-wc-product-external.php @@ -1,14 +1,15 @@ product_type = 'external'; - parent::__construct( $product ); + protected $extra_data = array( + 'product_url' => '', + 'button_text' => '', + ); + + /** + * Get internal type. + * @return string + */ + public function get_type() { + return 'external'; } + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the product object. + */ + + /** + * Get product url. + * + * @param string $context + * @return string + */ + public function get_product_url( $context = 'view' ) { + return esc_url( $this->get_prop( 'product_url', $context ) ); + } + + /** + * Get button text. + * + * @param string $context + * @return string + */ + public function get_button_text( $context = 'view' ) { + return $this->get_prop( 'button_text', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Functions for setting product data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. + */ + + /** + * Set product URL. + * + * @since 3.0.0 + * @param string $product_url Product URL. + */ + public function set_product_url( $product_url ) { + $this->set_prop( 'product_url', $product_url ); + } + + /** + * Set button text. + * + * @since 3.0.0 + * @param string $button_text Button text. + */ + public function set_button_text( $button_text ) { + $this->set_prop( 'button_text', $button_text ); + } + + /** + * External products cannot be stock managed. + * + * @since 3.0.0 + * @param bool + */ + public function set_manage_stock( $manage_stock ) { + $this->set_prop( 'manage_stock', false ); + + if ( true === $manage_stock ) { + $this->error( 'product_external_invalid_manage_stock', __( 'External products cannot be stock managed.', 'woocommerce' ) ); + } + } + + /** + * External products cannot be stock managed. + * + * @since 3.0.0 + * + * @param string $stock_status + */ + public function set_stock_status( $stock_status = '' ) { + $this->set_prop( 'stock_status', 'instock' ); + + if ( 'instock' !== $stock_status ) { + $this->error( 'product_external_invalid_stock_status', __( 'External products cannot be stock managed.', 'woocommerce' ) ); + } + } + + /** + * xternal products cannot be backordered. + * + * @since 3.0.0 + * @param string $backorders Options: 'yes', 'no' or 'notify'. + */ + public function set_backorders( $backorders ) { + $this->set_prop( 'backorders', 'no' ); + + if ( 'no' !== $backorders ) { + $this->error( 'product_external_invalid_backorders', __( 'External products cannot be backordered.', 'woocommerce' ) ); + } + } + + /* + |-------------------------------------------------------------------------- + | Other Actions + |-------------------------------------------------------------------------- + */ + /** * Returns false if the product cannot be bought. * @@ -47,42 +162,22 @@ class WC_Product_External extends WC_Product { } /** - * Get the add to cart button text for the single page + * Get the add to cart button text for the single page. * * @access public * @return string */ public function single_add_to_cart_text() { - return apply_filters( 'woocommerce_product_single_add_to_cart_text', $this->get_button_text(), $this ); + return apply_filters( 'woocommerce_product_single_add_to_cart_text', $this->get_button_text() ? $this->get_button_text() : _x( 'Buy product', 'placeholder', 'woocommerce' ), $this ); } /** - * Get the add to cart button text + * Get the add to cart button text. * * @access public * @return string */ public function add_to_cart_text() { - return apply_filters( 'woocommerce_product_single_add_to_cart_text', $this->get_button_text(), $this ); - } - - /** - * get_product_url function. - * - * @access public - * @return string - */ - public function get_product_url() { - return esc_url( $this->product_url ); - } - - /** - * get_button_text function. - * - * @access public - * @return string - */ - public function get_button_text() { - return $this->button_text ? $this->button_text : __( 'Buy product', 'woocommerce' ); + return apply_filters( 'woocommerce_product_add_to_cart_text', $this->get_button_text() ? $this->get_button_text() : _x( 'Buy product', 'placeholder', 'woocommerce' ), $this ); } } diff --git a/includes/class-wc-product-factory.php b/includes/class-wc-product-factory.php index c3a35e40f7d..8626900b0f0 100644 --- a/includes/class-wc-product-factory.php +++ b/includes/class-wc-product-factory.php @@ -1,12 +1,16 @@ get_product_id( $product_id ) ) { return false; - - if ( is_object ( $the_product ) ) { - $product_id = absint( $the_product->ID ); - $post_type = $the_product->post_type; } - if ( in_array( $post_type, array( 'product', 'product_variation' ) ) ) { - if ( isset( $args['product_type'] ) ) { - $product_type = $args['product_type']; - } elseif ( 'product_variation' == $post_type ) { - $product_type = 'variation'; - } else { - $terms = get_the_terms( $product_id, 'product_type' ); - $product_type = ! empty( $terms ) && isset( current( $terms )->name ) ? sanitize_title( current( $terms )->name ) : 'simple'; + $product_type = $this->get_product_type( $product_id ); + + // Backwards compatibility. + if ( ! empty( $deprecated ) ) { + wc_deprecated_argument( 'args', '3.0', 'Passing args to the product factory is deprecated. If you need to force a type, construct the product class directly.' ); + + if ( isset( $deprecated['product_type'] ) ) { + $product_type = $this->get_classname_from_product_type( $deprecated['product_type'] ); } - - // Create a WC coding standards compliant class name e.g. WC_Product_Type_Class instead of WC_Product_type-class - $classname = 'WC_Product_' . implode( '_', array_map( 'ucfirst', explode( '-', $product_type ) ) ); - } else { - $classname = false; - $product_type = false; } - // Filter classname so that the class can be overridden if extended. - $classname = apply_filters( 'woocommerce_product_class', $classname, $product_type, $post_type, $product_id ); + $classname = $this->get_product_classname( $product_id, $product_type ); - if ( ! class_exists( $classname ) ) + try { + return new $classname( $product_id, $deprecated ); + } catch ( Exception $e ) { + return false; + } + } + + /** + * Gets a product classname and allows filtering. Returns WC_Product_Simple if the class does not exist. + * + * @since 3.0.0 + * @param int $product_id + * @param string $product_type + * @return string + */ + public static function get_product_classname( $product_id, $product_type ) { + $classname = apply_filters( 'woocommerce_product_class', self::get_classname_from_product_type( $product_type ), $product_type, 'variation' === $product_type ? 'product_variation' : 'product', $product_id ); + + if ( ! $classname || ! class_exists( $classname ) ) { $classname = 'WC_Product_Simple'; + } - return new $classname( $the_product, $args ); + return $classname; + } + + /** + * Get the product type for a product. + * + * @since 3.0.0 + * @param int $product_id + * @return string|false + */ + public static function get_product_type( $product_id ) { + // Allow the overriding of the lookup in this function. Return the product type here. + $override = apply_filters( 'woocommerce_product_type_query', false, $product_id ); + if ( ! $override ) { + return WC_Data_Store::load( 'product' )->get_product_type( $product_id ); + } else { + return $override; + } + } + + /** + * Create a WC coding standards compliant class name e.g. WC_Product_Type_Class instead of WC_Product_type-class. + * + * @param string $product_type + * @return string|false + */ + public static function get_classname_from_product_type( $product_type ) { + return $product_type ? 'WC_Product_' . implode( '_', array_map( 'ucfirst', explode( '-', $product_type ) ) ) : false; + } + + /** + * Get the product ID depending on what was passed. + * + * @since 3.0.0 + * @param mixed $product + * @return int|bool false on failure + */ + private function get_product_id( $product ) { + if ( false === $product && isset( $GLOBALS['post'], $GLOBALS['post']->ID ) && 'product' === get_post_type( $GLOBALS['post']->ID ) ) { + return $GLOBALS['post']->ID; + } elseif ( is_numeric( $product ) ) { + return $product; + } elseif ( $product instanceof WC_Product ) { + return $product->get_id(); + } elseif ( ! empty( $product->ID ) ) { + return $product->ID; + } else { + return false; + } } } diff --git a/includes/class-wc-product-grouped.php b/includes/class-wc-product-grouped.php index 67f10e19fc4..dadf9fe2caa 100644 --- a/includes/class-wc-product-grouped.php +++ b/includes/class-wc-product-grouped.php @@ -1,39 +1,40 @@ array(), + ); /** - * __construct function. - * - * @access public - * @param mixed $product + * Get internal type. + * @return string */ - public function __construct( $product ) { - $this->product_type = 'grouped'; - parent::__construct( $product ); + public function get_type() { + return 'grouped'; } /** - * Get the add to cart button text + * Get the add to cart button text. * * @access public * @return string @@ -42,118 +43,31 @@ class WC_Product_Grouped extends WC_Product { return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'View products', 'woocommerce' ), $this ); } - /** - * Get total stock. - * - * This is the stock of parent and children combined. - * - * @access public - * @return int - */ - public function get_total_stock() { - - if ( empty( $this->total_stock ) ) { - - $transient_name = 'wc_product_total_stock_' . $this->id; - - if ( false === ( $this->total_stock = get_transient( $transient_name ) ) ) { - $this->total_stock = $this->stock; - - if ( sizeof( $this->get_children() ) > 0 ) { - foreach ($this->get_children() as $child_id) { - $stock = get_post_meta( $child_id, '_stock', true ); - - if ( $stock != '' ) { - $this->total_stock += wc_stock_amount( $stock ); - } - } - } - - set_transient( $transient_name, $this->total_stock, YEAR_IN_SECONDS ); - } - } - - return wc_stock_amount( $this->total_stock ); - } - - /** - * Return the products children posts. - * - * @access public - * @return array - */ - public function get_children() { - - if ( ! is_array( $this->children ) ) { - - $this->children = array(); - - $transient_name = 'wc_product_children_ids_' . $this->id; - - if ( false === ( $this->children = get_transient( $transient_name ) ) ) { - - $this->children = get_posts( 'post_parent=' . $this->id . '&post_type=product&orderby=menu_order&order=ASC&fields=ids&post_status=publish&numberposts=-1' ); - - set_transient( $transient_name, $this->children, YEAR_IN_SECONDS ); - } - } - - return (array) $this->children; - } - - - /** - * get_child function. - * - * @access public - * @param mixed $child_id - * @return object WC_Product or WC_Product_variation - */ - public function get_child( $child_id ) { - return get_product( $child_id ); - } - - - /** - * Returns whether or not the product has any child product. - * - * @access public - * @return bool - */ - public function has_child() { - return sizeof( $this->get_children() ) ? true : false; - } - - /** * Returns whether or not the product is on sale. * - * @access public + * @param string $context What the value is for. Valid values are view and edit. * @return bool */ - public function is_on_sale() { - if ( $this->has_child() ) { + public function is_on_sale( $context = 'view' ) { + global $wpdb; - foreach ( $this->get_children() as $child_id ) { - $sale_price = get_post_meta( $child_id, '_sale_price', true ); - if ( $sale_price !== "" && $sale_price >= 0 ) - return true; + $children = array_filter( array_map( 'wc_get_product', $this->get_children( $context ) ), 'wc_products_array_filter_visible_grouped' ); + $on_sale = false; + + foreach ( $children as $child ) { + if ( $child->is_on_sale() ) { + $on_sale = true; + break; } - - } else { - - if ( $this->sale_price && $this->sale_price == $this->price ) - return true; - } - return false; - } + return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; + } /** * Returns false if the product cannot be bought. * - * @access public * @return bool */ public function is_purchasable() { @@ -170,12 +84,13 @@ class WC_Product_Grouped extends WC_Product { public function get_price_html( $price = '' ) { $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); $child_prices = array(); + $children = array_filter( array_map( 'wc_get_product', $this->get_children() ), 'wc_products_array_filter_visible_grouped' ); - foreach ( $this->get_children() as $child_id ) - $child_prices[] = get_post_meta( $child_id, '_price', true ); - - $child_prices = array_unique( $child_prices ); - $get_price_method = 'get_price_' . $tax_display_mode . 'uding_tax'; + foreach ( $children as $child ) { + if ( '' !== $child->get_price() ) { + $child_prices[] = 'incl' === $tax_display_mode ? wc_get_price_including_tax( $child ) : wc_get_price_excluding_tax( $child ); + } + } if ( ! empty( $child_prices ) ) { $min_price = min( $child_prices ); @@ -185,22 +100,82 @@ class WC_Product_Grouped extends WC_Product { $max_price = ''; } - if ( $min_price ) { - if ( $min_price == $max_price ) { - $display_price = wc_price( $this->$get_price_method( 1, $min_price ) ); + if ( '' !== $min_price ) { + $price = $min_price !== $max_price ? sprintf( _x( '%1$s–%2$s', 'Price range: from-to', 'woocommerce' ), wc_price( $min_price ), wc_price( $max_price ) ) : wc_price( $min_price ); + $is_free = ( 0 == $min_price && 0 == $max_price ); + + if ( $is_free ) { + $price = apply_filters( 'woocommerce_grouped_free_price_html', __( 'Free!', 'woocommerce' ), $this ); } else { - $from = wc_price( $this->$get_price_method( 1, $min_price ) ); - $to = wc_price( $this->$get_price_method( 1, $max_price ) ); - $display_price = sprintf( _x( '%1$s–%2$s', 'Price range: from-to', 'woocommerce' ), $from, $to ); + $price = apply_filters( 'woocommerce_grouped_price_html', $price . $this->get_price_suffix(), $this, $child_prices ); } - - $price .= $display_price . $this->get_price_suffix(); - - $price = apply_filters( 'woocommerce_grouped_price_html', $price, $this ); } else { $price = apply_filters( 'woocommerce_grouped_empty_price_html', '', $this ); } return apply_filters( 'woocommerce_get_price_html', $price, $this ); } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the product object. + */ + + /** + * Return the children of this product. + * + * @param string $context + * @return array + */ + public function get_children( $context = 'view' ) { + return $this->get_prop( 'children', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the product object. + */ + + /** + * Return the children of this product. + * + * @param array $children + */ + public function set_children( $children ) { + $this->set_prop( 'children', array_filter( wp_parse_id_list( (array) $children ) ) ); + } + + /* + |-------------------------------------------------------------------------- + | Sync with children. + |-------------------------------------------------------------------------- + */ + + /** + * Sync a grouped product with it's children. These sync functions sync + * upwards (from child to parent) when the variation is saved. + * + * @param WC_Product|int $product Product object or ID for which you wish to sync. + * @param bool $save If true, the prouduct object will be saved to the DB before returning it. + * @return WC_Product Synced product object. + */ + public static function sync( $product, $save = true ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + if ( is_a( $product, 'WC_Product_Grouped' ) ) { + $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); + $data_store->sync_price( $product ); + if ( $save ) { + $product->save(); + } + } + return $product; + } } diff --git a/includes/class-wc-product-simple.php b/includes/class-wc-product-simple.php index c4ce6210e61..33ee968fde4 100644 --- a/includes/class-wc-product-simple.php +++ b/includes/class-wc-product-simple.php @@ -1,14 +1,15 @@ product_type = 'simple'; + public function __construct( $product = 0 ) { + $this->supports[] = 'ajax_add_to_cart'; parent::__construct( $product ); } + /** + * Get internal type. + * @return string + */ + public function get_type() { + return 'simple'; + } + /** * Get the add to url used mainly in loops. * - * @access public * @return string */ public function add_to_cart_url() { @@ -39,61 +46,13 @@ class WC_Product_Simple extends WC_Product { } /** - * Get the add to cart button text + * Get the add to cart button text. * - * @access public * @return string */ public function add_to_cart_text() { - $text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add to cart', 'woocommerce' ) : __( 'Read More', 'woocommerce' ); + $text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add to cart', 'woocommerce' ) : __( 'Read more', 'woocommerce' ); return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this ); } - - /** - * Get the title of the post. - * - * @access public - * @return string - */ - public function get_title() { - - $title = $this->post->post_title; - - if ( $this->get_parent() > 0 ) { - $title = get_the_title( $this->get_parent() ) . ' → ' . $title; - } - - return apply_filters( 'woocommerce_product_title', $title, $this ); - } - - /** - * Sync grouped products with the children lowest price (so they can be sorted by price accurately). - * - * @access public - * @return void - */ - public function grouped_product_sync() { - if ( ! $this->get_parent() ) return; - - $children_by_price = get_posts( array( - 'post_parent' => $this->get_parent(), - 'orderby' => 'meta_value_num', - 'order' => 'asc', - 'meta_key' => '_price', - 'posts_per_page' => 1, - 'post_type' => 'product', - 'fields' => 'ids' - )); - if ( $children_by_price ) { - foreach ( $children_by_price as $child ) { - $child_price = get_post_meta( $child, '_price', true ); - update_post_meta( $this->get_parent(), '_price', $child_price ); - } - } - - delete_transient( 'wc_products_onsale' ); - - do_action( 'woocommerce_grouped_product_sync', $this->id, $children_by_price ); - } -} \ No newline at end of file +} diff --git a/includes/class-wc-product-variable.php b/includes/class-wc-product-variable.php index 413dec64f35..7ab11982e1e 100644 --- a/includes/class-wc-product-variable.php +++ b/includes/class-wc-product-variable.php @@ -1,623 +1,569 @@ product_type = 'variable'; - parent::__construct( $product ); + protected $visible_children = array(); + + /** + * Array of variation attributes IDs. Determined by children. + * + * @var array + */ + protected $variation_attributes = array(); + + /** + * Get internal type. + * + * @return string + */ + public function get_type() { + return 'variable'; } + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + /** - * Get the add to cart button text + * Get the add to cart button text. * - * @access public * @return string */ public function add_to_cart_text() { - return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Select options', 'woocommerce' ), $this ); + return apply_filters( 'woocommerce_product_add_to_cart_text', $this->is_purchasable() ? __( 'Select options', 'woocommerce' ) : __( 'Read more', 'woocommerce' ), $this ); } - /** - * Get total stock. - * - * This is the stock of parent and children combined. - * - * @access public - * @return int - */ - public function get_total_stock() { - if ( empty( $this->total_stock ) ) { - $transient_name = 'wc_product_total_stock_' . $this->id; - - if ( false === ( $this->total_stock = get_transient( $transient_name ) ) ) { - $this->total_stock = max( 0, wc_stock_amount( $this->stock ) ); - - if ( sizeof( $this->get_children() ) > 0 ) { - foreach ( $this->get_children() as $child_id ) { - if ( 'yes' === get_post_meta( $child_id, '_manage_stock', true ) ) { - $stock = get_post_meta( $child_id, '_stock', true ); - $this->total_stock += max( 0, wc_stock_amount( $stock ) ); - } - } - } - set_transient( $transient_name, $this->total_stock, YEAR_IN_SECONDS ); - } - } - return wc_stock_amount( $this->total_stock ); - } - /** - * Set stock level of the product. + * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. * - * @param mixed $amount (default: null) - * @param string $mode can be set, add, or subtract - * @return int Stock + * @param bool $include_taxes Should taxes be included in the prices. + * @return array() Array of RAW prices, regular prices, and sale prices with keys set to variation ID. */ - public function set_stock( $amount = null, $mode = 'set' ) { - $this->total_stock = ''; - delete_transient( 'wc_product_total_stock_' . $this->id ); - return parent::set_stock( $amount, $mode ); - } + public function get_variation_prices( $include_taxes = false ) { + $prices = $this->data_store->read_price_data( $this, $include_taxes ); - /** - * Performed after a stock level change at product level - */ - protected function check_stock_status() { - $set_child_stock_status = ''; - - if ( ! $this->backorders_allowed() && $this->get_stock_quantity() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) { - $set_child_stock_status = 'outofstock'; - } elseif ( $this->backorders_allowed() || $this->get_stock_quantity() > get_option( 'woocommerce_notify_no_stock_amount' ) ) { - $set_child_stock_status = 'instock'; + foreach ( $prices as $price_key => $variation_prices ) { + $prices[ $price_key ] = $this->sort_variation_prices( $variation_prices ); } - if ( $set_child_stock_status ) { - foreach ( $this->get_children() as $child_id ) { - if ( 'yes' !== get_post_meta( $child_id, '_manage_stock', true ) ) { - wc_update_product_stock_status( $child_id, $set_child_stock_status ); - } - } - - // Children statuses changed, so sync self - self::sync_stock_status( $this->id ); - } - } - - /** - * set_stock_status function. - * - * @access public - * @return void - */ - public function set_stock_status( $status ) { - $status = ( 'outofstock' === $status ) ? 'outofstock' : 'instock'; - - if ( update_post_meta( $this->id, '_stock_status', $status ) ) { - do_action( 'woocommerce_product_set_stock_status', $this->id, $status ); - } - } - - /** - * Return the products children posts. - * - * @param boolean $visible_only Only return variations which are not hidden - * @return array of children ids - */ - public function get_children( $visible_only = false ) { - if ( ! is_array( $this->children ) ) { - $this->children = array(); - $transient_name = 'wc_product_children_ids_' . $this->id; - - if ( false === ( $this->children = get_transient( $transient_name ) ) ) { - $args = array( - 'post_parent' => $this->id, - 'post_type' => 'product_variation', - 'orderby' => 'menu_order', - 'order' => 'ASC', - 'fields' => 'ids', - 'post_status' => 'any', - 'numberposts' => -1 - ); - - $this->children = get_posts( $args ); - - set_transient( $transient_name, $this->children, YEAR_IN_SECONDS ); - } - } - - if ( $visible_only ) { - $children = array(); - foreach( $this->children as $child_id ) { - if ( 'yes' === get_post_meta( $child_id, '_manage_stock', true ) ) { - if ( 'instock' === get_post_meta( $child_id, '_stock_status', true ) ) { - $children[] = $child_id; - } - } elseif ( $this->is_in_stock() ) { - $children[] = $child_id; - } - } - } else { - $children = $this->children; - } - - return $children; - } - - - /** - * get_child function. - * - * @access public - * @param mixed $child_id - * @return object WC_Product or WC_Product_variation - */ - public function get_child( $child_id ) { - return get_product( $child_id, array( - 'parent_id' => $this->id, - 'parent' => $this - ) ); - } - - - /** - * Returns whether or not the product has any child product. - * - * @access public - * @return bool - */ - public function has_child() { - return sizeof( $this->get_children() ) ? true : false; - } - - - /** - * Returns whether or not the product is on sale. - * - * @access public - * @return bool - */ - public function is_on_sale() { - if ( $this->has_child() ) { - - foreach ( $this->get_children( true ) as $child_id ) { - $price = get_post_meta( $child_id, '_price', true ); - $sale_price = get_post_meta( $child_id, '_sale_price', true ); - if ( $sale_price !== "" && $sale_price >= 0 && $sale_price == $price ) - return true; - } - - } - return false; + return $prices; } /** * Get the min or max variation regular price. - * @param string $min_or_max - min or max - * @param boolean $display Whether the value is going to be displayed + * + * @param string $min_or_max Min or max price. + * @param boolean $include_taxes Should the price include taxes? * @return string */ - public function get_variation_regular_price( $min_or_max = 'min', $display = false ) { - $variation_id = get_post_meta( $this->id, '_' . $min_or_max . '_regular_price_variation_id', true ); - - if ( ! $variation_id ) { - return false; - } - - $price = get_post_meta( $variation_id, '_regular_price', true ); - - if ( $display ) { - $variation = $this->get_child( $variation_id ); - $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); - $price = $tax_display_mode == 'incl' ? $variation->get_price_including_tax( 1, $price ) : $variation->get_price_excluding_tax( 1, $price ); - } - - return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $display ); + public function get_variation_regular_price( $min_or_max = 'min', $include_taxes = false ) { + $prices = $this->get_variation_prices( $include_taxes ); + $price = 'min' === $min_or_max ? current( $prices['regular_price'] ) : end( $prices['regular_price'] ); + return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $include_taxes ); } /** * Get the min or max variation sale price. - * @param string $min_or_max - min or max - * @param boolean $display Whether the value is going to be displayed + * + * @param string $min_or_max Min or max price. + * @param boolean $include_taxes Should the price include taxes? * @return string */ - public function get_variation_sale_price( $min_or_max = 'min', $display = false ) { - $variation_id = get_post_meta( $this->id, '_' . $min_or_max . '_sale_price_variation_id', true ); - - if ( ! $variation_id ) { - return false; - } - - $price = get_post_meta( $variation_id, '_sale_price', true ); - - if ( $display ) { - $variation = $this->get_child( $variation_id ); - $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); - $price = $tax_display_mode == 'incl' ? $variation->get_price_including_tax( 1, $price ) : $variation->get_price_excluding_tax( 1, $price ); - } - - return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $display ); + public function get_variation_sale_price( $min_or_max = 'min', $include_taxes = false ) { + $prices = $this->get_variation_prices( $include_taxes ); + $price = 'min' === $min_or_max ? current( $prices['sale_price'] ) : end( $prices['sale_price'] ); + return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $include_taxes ); } /** * Get the min or max variation (active) price. - * @param string $min_or_max - min or max - * @param boolean $display Whether the value is going to be displayed + * + * @param string $min_or_max Min or max price. + * @param boolean $include_taxes Should the price include taxes? * @return string */ - public function get_variation_price( $min_or_max = 'min', $display = false ) { - $variation_id = get_post_meta( $this->id, '_' . $min_or_max . '_price_variation_id', true ); - - if ( $display ) { - $variation = $this->get_child( $variation_id ); - - if ( $variation ) { - $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); - $price = $tax_display_mode == 'incl' ? $variation->get_price_including_tax() : $variation->get_price_excluding_tax(); - } else { - $price = ''; - } - } else { - $price = get_post_meta( $variation_id, '_price', true ); - } - - return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $display ); + public function get_variation_price( $min_or_max = 'min', $include_taxes = false ) { + $prices = $this->get_variation_prices( $include_taxes ); + $price = 'min' === $min_or_max ? current( $prices['price'] ) : end( $prices['price'] ); + return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $include_taxes ); } /** * Returns the price in html format. * - * @access public + * Note: Variable prices do not show suffixes like other product types. This + * is due to some things like tax classes being set at variation level which + * could differ from the parent price. The only way to show accurate prices + * would be to load the variation and get IT's price, which adds extra + * overhead and still has edge cases where the values would be inaccurate. + * + * Additionally, ranges of prices no longer show 'striked out' sale prices + * due to the strings being very long and unclear/confusing. A single range + * is shown instead. + * * @param string $price (default: '') * @return string */ public function get_price_html( $price = '' ) { + $prices = $this->get_variation_prices( true ); - // Ensure variation prices are synced with variations - if ( $this->get_variation_regular_price( 'min' ) === false || $this->get_variation_price( 'min' ) === false || $this->get_variation_price( 'min' ) === '' || $this->get_price() === '' ) { - $this->variable_product_sync( $this->id ); - } - - // Get the price - if ( $this->get_price() === '' ) { - + if ( empty( $prices['price'] ) ) { $price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this ); - } else { + $min_price = current( $prices['price'] ); + $max_price = end( $prices['price'] ); + $min_reg_price = current( $prices['regular_price'] ); + $max_reg_price = end( $prices['regular_price'] ); - // Main price - $prices = array( $this->get_variation_price( 'min', true ), $this->get_variation_price( 'max', true ) ); - $price = $prices[0] !== $prices[1] ? sprintf( _x( '%1$s–%2$s', 'Price range: from-to', 'woocommerce' ), wc_price( $prices[0] ), wc_price( $prices[1] ) ) : wc_price( $prices[0] ); - - // Sale - $prices = array( $this->get_variation_regular_price( 'min', true ), $this->get_variation_regular_price( 'max', true ) ); - sort( $prices ); - $saleprice = $prices[0] !== $prices[1] ? sprintf( _x( '%1$s–%2$s', 'Price range: from-to', 'woocommerce' ), wc_price( $prices[0] ), wc_price( $prices[1] ) ) : wc_price( $prices[0] ); - - if ( $price !== $saleprice ) { - $price = apply_filters( 'woocommerce_variable_sale_price_html', $this->get_price_html_from_to( $saleprice, $price ) . $this->get_price_suffix(), $this ); + if ( $min_price !== $max_price ) { + $price = wc_format_price_range( $min_price, $max_price ); + } elseif ( $this->is_on_sale() && $min_reg_price === $max_reg_price ) { + $price = wc_format_sale_price( wc_price( $max_reg_price ), wc_price( $min_price ) ); } else { - $price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this ); + $price = wc_price( $min_price ); } + $price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this ); } return apply_filters( 'woocommerce_get_price_html', $price, $this ); } + /** + * Get the suffix to display after prices > 0. + * + * This is skipped if the suffix + * has dynamic values such as {price_excluding_tax} for variable products. + * @see get_price_html for an explanation as to why. + * + * @param string $price to calculate, left blank to just use get_price() + * @param integer $qty passed on to get_price_including_tax() or get_price_excluding_tax() + * @return string + */ + public function get_price_suffix( $price = '', $qty = 1 ) { + $suffix = get_option( 'woocommerce_price_display_suffix' ); - /** - * Return an array of attributes used for variations, as well as their possible values. - * - * @access public - * @return array of attributes and their available values - */ - public function get_variation_attributes() { + if ( strstr( $suffix, '{' ) ) { + return apply_filters( 'woocommerce_get_price_suffix', '', $this, $price, $qty ); + } else { + return parent::get_price_suffix( $price, $qty ); + } + } - $variation_attributes = array(); + /** + * Return a products child ids. + * + * @param bool|string $visible_only + * + * @return array Children ids + */ + public function get_children( $visible_only = '' ) { + if ( is_bool( $visible_only ) ) { + wc_deprecated_argument( 'visible_only', '3.0', 'WC_Product_Variable::get_visible_children' ); + return $visible_only ? $this->get_visible_children() : $this->get_children(); + } + return apply_filters( 'woocommerce_get_children', $this->children, $this, false ); + } - if ( ! $this->has_child() ) - return $variation_attributes; + /** + * Return a products child ids - visible only. + * + * @since 3.0.0 + * @return array Children ids + */ + public function get_visible_children() { + return apply_filters( 'woocommerce_get_children', $this->visible_children, $this, true ); + } - $attributes = $this->get_attributes(); + /** + * Return an array of attributes used for variations, as well as their possible values. + * + * @return array Attributes and their available values + */ + public function get_variation_attributes() { + return $this->variation_attributes; + } - foreach ( $attributes as $attribute ) { - if ( ! $attribute['is_variation'] ) - continue; + /** + * If set, get the default attributes for a variable product. + * + * @param string $attribute_name + * @return string + */ + public function get_variation_default_attribute( $attribute_name ) { + $defaults = $this->get_default_attributes(); + $attribute_name = sanitize_title( $attribute_name ); + return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : ''; + } - $values = array(); - $attribute_field_name = 'attribute_' . sanitize_title( $attribute['name'] ); + /** + * Variable products themselves cannot be downloadable. + * + * @param string $context + * + * @return bool + */ + public function get_downloadable( $context = 'view' ) { + return false; + } - foreach ( $this->get_children() as $child_id ) { + /** + * Variable products themselves cannot be virtual. + * + * @param string $context + * + * @return bool + */ + public function get_virtual( $context = 'view' ) { + return false; + } - $variation = $this->get_child( $child_id ); - - if ( ! empty( $variation->variation_id ) ) { - - if ( ! $variation->variation_is_visible() ) - continue; // Disabled or hidden - - $child_variation_attributes = $variation->get_variation_attributes(); - - foreach ( $child_variation_attributes as $name => $value ) - if ( $name == $attribute_field_name ) - $values[] = sanitize_title( $value ); - } - } - - // empty value indicates that all options for given attribute are available - if ( in_array( '', $values ) ) { - - $values = array(); - - // Get all options - if ( $attribute['is_taxonomy'] ) { - $post_terms = wp_get_post_terms( $this->id, $attribute['name'] ); - foreach ( $post_terms as $term ) - $values[] = $term->slug; - } else { - $values = array_map( 'trim', explode( WC_DELIMITER, $attribute['value'] ) ); - } - - $values = array_unique( $values ); - - // Order custom attributes (non taxonomy) as defined - } elseif ( ! $attribute['is_taxonomy'] ) { - - $option_names = array_map( 'trim', explode( WC_DELIMITER, $attribute['value'] ) ); - $option_slugs = $values; - $values = array(); - - foreach ( $option_names as $option_name ) { - if ( in_array( sanitize_title( $option_name ), $option_slugs ) ) - $values[] = $option_name; - } - } - - $variation_attributes[ $attribute['name'] ] = array_unique( $values ); - } - - return $variation_attributes; - } - - /** - * If set, get the default attributes for a variable product. - * - * @access public - * @return array - */ - public function get_variation_default_attributes() { - - $default = isset( $this->default_attributes ) ? $this->default_attributes : ''; - - return apply_filters( 'woocommerce_product_default_attributes', (array) maybe_unserialize( $default ), $this ); - } - - /** - * Get an array of available variations for the current product. - * - * @access public - * @return array - */ - public function get_available_variations() { - - $available_variations = array(); + /** + * Get an array of available variations for the current product. + * + * @return array + */ + public function get_available_variations() { + $available_variations = array(); foreach ( $this->get_children() as $child_id ) { + $variation = wc_get_product( $child_id ); - $variation = $this->get_child( $child_id ); - - if ( empty( $variation->variation_id ) || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { + // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked + if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { continue; } - - $variation_attributes = $variation->get_variation_attributes(); - $availability = $variation->get_availability(); - $availability_html = empty( $availability['availability'] ) ? '' : apply_filters( 'woocommerce_stock_html', '

    '. wp_kses_post( $availability['availability'] ).'

    ', wp_kses_post( $availability['availability'] ) ); - if ( has_post_thumbnail( $variation->get_variation_id() ) ) { - $attachment_id = get_post_thumbnail_id( $variation->get_variation_id() ); - - $attachment = wp_get_attachment_image_src( $attachment_id, apply_filters( 'single_product_large_thumbnail_size', 'shop_single' ) ); - $image = $attachment ? current( $attachment ) : ''; - - $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); - $image_link = $attachment ? current( $attachment ) : ''; - - $image_title = get_the_title( $attachment_id ); - $image_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); - } else { - $image = $image_link = $image_title = $image_alt = ''; + // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price) + if ( apply_filters( 'woocommerce_hide_invisible_variations', false, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { + continue; } - $available_variations[] = apply_filters( 'woocommerce_available_variation', array( - 'variation_id' => $child_id, - 'variation_is_visible' => $variation->variation_is_visible(), - 'is_purchasable' => $variation->is_purchasable(), - 'attributes' => $variation_attributes, - 'image_src' => $image, - 'image_link' => $image_link, - 'image_title' => $image_title, - 'image_alt' => $image_alt, - 'price_html' => $variation->get_price() === "" || $this->get_variation_price( 'min' ) !== $this->get_variation_price( 'max' ) ? '' . $variation->get_price_html() . '' : '', - 'availability_html' => $availability_html, - 'sku' => $variation->get_sku(), - 'weight' => $variation->get_weight() . ' ' . esc_attr( get_option('woocommerce_weight_unit' ) ), - 'dimensions' => $variation->get_dimensions(), - 'min_qty' => 1, - 'max_qty' => $variation->backorders_allowed() ? '' : $variation->stock, - 'backorders_allowed' => $variation->backorders_allowed(), - 'is_in_stock' => $variation->is_in_stock(), - 'is_downloadable' => $variation->is_downloadable() , - 'is_virtual' => $variation->is_virtual(), - 'is_sold_individually' => $variation->is_sold_individually() ? 'yes' : 'no', - ), $this, $variation ); + $available_variations[] = $this->get_available_variation( $variation ); } return $available_variations; - } - - /** - * Sync variable product prices with the children lowest/highest prices. - */ - public function variable_product_sync( $product_id = '' ) { - if ( empty( $product_id ) ) - $product_id = $this->id; - - // Sync prices with children - self::sync( $product_id ); - - // Re-load prices - $this->price = get_post_meta( $product_id, '_price', true ); - - foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) { - $min_variation_id_key = "min_{$price_type}_variation_id"; - $max_variation_id_key = "max_{$price_type}_variation_id"; - $min_price_key = "_min_variation_{$price_type}"; - $max_price_key = "_max_variation_{$price_type}"; - $this->$min_variation_id_key = get_post_meta( $product_id, '_' . $min_variation_id_key, true ); - $this->$max_variation_id_key = get_post_meta( $product_id, '_' . $max_variation_id_key, true ); - $this->$min_price_key = get_post_meta( $product_id, '_' . $min_price_key, true ); - $this->$max_price_key = get_post_meta( $product_id, '_' . $max_price_key, true ); - } } /** - * Sync variable product stock status with children - * @param int $product_id + * Returns an array of data for a variation. Used in the add to cart form. + * @since 2.4.0 + * @param WC_Product $variation Variation product object or ID + * @return array */ - public static function sync_stock_status( $product_id ) { - $children = get_posts( array( - 'post_parent' => $product_id, - 'posts_per_page'=> -1, - 'post_type' => 'product_variation', - 'fields' => 'ids', - 'post_status' => 'publish' - ) ); - - $stock_status = 'outofstock'; - - foreach ( $children as $child_id ) { - $child_stock_status = get_post_meta( $child_id, '_stock_status', true ); - $child_stock_status = $child_stock_status ? $child_stock_status : 'instock'; - if ( 'instock' === $child_stock_status ) { - $stock_status = 'instock'; - break; - } + public function get_available_variation( $variation ) { + if ( is_numeric( $variation ) ) { + $variation = wc_get_product( $variation ); } - wc_update_product_stock_status( $product_id, $stock_status ); + // See if prices should be shown for each variation after selection. + $show_variation_price = apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === "" || $this->get_variation_sale_price( 'min' ) !== $this->get_variation_sale_price( 'max' ) || $this->get_variation_regular_price( 'min' ) !== $this->get_variation_regular_price( 'max' ), $this, $variation ); + + return apply_filters( 'woocommerce_available_variation', array( + 'attributes' => $variation->get_variation_attributes(), + 'availability_html' => wc_get_stock_html( $variation ), + 'backorders_allowed' => $variation->backorders_allowed(), + 'dimensions' => wc_format_dimensions( $variation->get_dimensions( false ) ), + 'dimensions_html' => wc_format_dimensions( $variation->get_dimensions( false ) ), + 'display_price' => wc_get_price_to_display( $variation ), + 'display_regular_price' => wc_get_price_to_display( $variation, array( 'price' => $variation->get_regular_price() ) ), + 'image' => wc_get_product_attachment_props( $variation->get_image_id() ), + 'image_id' => $variation->get_image_id(), + 'is_downloadable' => $variation->is_downloadable(), + 'is_in_stock' => $variation->is_in_stock(), + 'is_purchasable' => $variation->is_purchasable(), + 'is_sold_individually' => $variation->is_sold_individually() ? 'yes' : 'no', + 'is_virtual' => $variation->is_virtual(), + 'max_qty' => 0 < $variation->get_max_purchase_quantity() ? $variation->get_max_purchase_quantity() : '', + 'min_qty' => $variation->get_min_purchase_quantity(), + 'price_html' => $show_variation_price ? '' . $variation->get_price_html() . '' : '', + 'sku' => $variation->get_sku(), + 'variation_description' => wc_format_content( $variation->get_description() ), + 'variation_id' => $variation->get_id(), + 'variation_is_active' => $variation->variation_is_active(), + 'variation_is_visible' => $variation->variation_is_visible(), + 'weight' => wc_format_weight( $variation->get_weight() ), + 'weight_html' => wc_format_weight( $variation->get_weight() ), + ), $this, $variation ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Sets an array of variation attributes. + * + * @since 3.0.0 + * @param array + */ + public function set_variation_attributes( $variation_attributes ) { + $this->variation_attributes = $variation_attributes; } /** - * Sync the variable product with it's children + * Sets an array of children for the product. + * + * @since 3.0.0 + * @param array */ - public static function sync( $product_id ) { - global $wpdb; + public function set_children( $children ) { + $this->children = array_filter( wp_parse_id_list( (array) $children ) ); + } - $children = get_posts( array( - 'post_parent' => $product_id, - 'posts_per_page'=> -1, - 'post_type' => 'product_variation', - 'fields' => 'ids', - 'post_status' => 'publish' - ) ); + /** + * Sets an array of visible children only. + * + * @since 3.0.0 + * @param array + */ + public function set_visible_children( $visible_children ) { + $this->visible_children = array_filter( wp_parse_id_list( (array) $visible_children ) ); + } - // No published variations - update parent post status. Use $wpdb to prevent endless loop on save_post hooks. - if ( ! $children && get_post_status( $product_id ) == 'publish' ) { - $wpdb->update( $wpdb->posts, array( 'post_status' => 'draft' ), array( 'ID' => $product_id ) ); + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + */ - if ( is_admin() ) { - WC_Admin_Meta_Boxes::add_error( __( 'This variable product has no active variations so cannot be published. Changing status to draft.', 'woocommerce' ) ); - } + /** + * Ensure properties are set correctly before save. + * @since 3.0.0 + */ + public function validate_props() { + // Before updating, ensure stock props are all aligned. Qty and backorders are not needed if not stock managed. + if ( ! $this->get_manage_stock() ) { + $this->set_stock_quantity( '' ); + $this->set_backorders( 'no' ); + $this->set_stock_status( $this->child_is_in_stock() ? 'instock' : 'outofstock' ); - // Loop the variations + // If backorders are enabled, always in stock. + } elseif ( 'no' !== $this->get_backorders() ) { + $this->set_stock_status( 'instock' ); + + // If we are stock managing and we don't have stock, force out of stock status. + } elseif ( $this->get_stock_quantity() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) { + $this->set_stock_status( 'outofstock' ); + + // If the stock level is changing and we do now have enough, force in stock status. + } elseif ( $this->get_stock_quantity() > get_option( 'woocommerce_notify_no_stock_amount' ) && array_key_exists( 'stock_quantity', $this->get_changes() ) ) { + $this->set_stock_status( 'instock' ); + + // Otherwise revert to status the children have. } else { - // Main active prices - $min_price = null; - $max_price = null; - $min_price_id = null; - $max_price_id = null; + $this->set_stock_status( $this->child_is_in_stock() ? 'instock' : 'outofstock' ); + } + } - // Regular prices - $min_regular_price = null; - $max_regular_price = null; - $min_regular_price_id = null; - $max_regular_price_id = null; + /** + * Save data (either create or update depending on if we are working on an existing product). + * + * @since 3.0.0 + */ + public function save() { + $this->validate_props(); + if ( $this->data_store ) { + // Trigger action before saving to the DB. Allows you to adjust object props before save. + do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); - // Sale prices - $min_sale_price = null; - $max_sale_price = null; - $min_sale_price_id = null; - $max_sale_price_id = null; + // Get names before save. + $previous_name = $this->data['name']; + $new_name = $this->get_name( 'edit' ); - foreach ( array( 'price', 'regular_price', 'sale_price' ) as $price_type ) { - foreach ( $children as $child_id ) { - $child_price = get_post_meta( $child_id, '_' . $price_type, true ); - - // Skip non-priced variations - if ( $child_price === '' ) { - continue; - } - - // Skip hidden variations - if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { - $stock = get_post_meta( $child_id, '_stock', true ); - if ( $stock !== "" && $stock <= get_option( 'woocommerce_notify_no_stock_amount' ) ) { - continue; - } - } - - // Find min price - if ( is_null( ${"min_{$price_type}"} ) || $child_price < ${"min_{$price_type}"} ) { - ${"min_{$price_type}"} = $child_price; - ${"min_{$price_type}_id"} = $child_id; - } - - // Find max price - if ( $child_price > ${"max_{$price_type}"} ) { - ${"max_{$price_type}"} = $child_price; - ${"max_{$price_type}_id"} = $child_id; - } - } - - // Store prices - update_post_meta( $product_id, '_min_variation_' . $price_type, ${"min_{$price_type}"} ); - update_post_meta( $product_id, '_max_variation_' . $price_type, ${"max_{$price_type}"} ); - - // Store ids - update_post_meta( $product_id, '_min_' . $price_type . '_variation_id', ${"min_{$price_type}_id"} ); - update_post_meta( $product_id, '_max_' . $price_type . '_variation_id', ${"max_{$price_type}_id"} ); + if ( $this->get_id() ) { + $this->data_store->update( $this ); + } else { + $this->data_store->create( $this ); } - // The VARIABLE PRODUCT price should equal the min price of any type - update_post_meta( $product_id, '_price', $min_price ); - delete_transient( 'wc_products_onsale' ); - - do_action( 'woocommerce_variable_product_sync', $product_id, $children ); + $this->data_store->sync_variation_names( $this, $previous_name, $new_name ); + $this->data_store->sync_managed_variation_stock_status( $this ); + + return $this->get_id(); } } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + */ + + /** + * Returns whether or not the product is on sale. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function is_on_sale( $context = 'view' ) { + $prices = $this->get_variation_prices(); + $on_sale = $prices['regular_price'] !== $prices['sale_price'] && $prices['sale_price'] === $prices['price']; + + return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; + } + + /** + * Is a child in stock? + * @return boolean + */ + public function child_is_in_stock() { + return $this->data_store->child_is_in_stock( $this ); + } + + /** + * Does a child have a weight set? + * @return boolean + */ + public function child_has_weight() { + $transient_name = 'wc_child_has_weight_' . $this->get_id(); + $has_weight = get_transient( $transient_name ); + + if ( false === $has_weight ) { + $has_weight = $this->data_store->child_has_weight( $this ); + set_transient( $transient_name, $has_weight, DAY_IN_SECONDS * 30 ); + } + return (bool) $has_weight; + } + + /** + * Does a child have dimensions set? + * @return boolean + */ + public function child_has_dimensions() { + $transient_name = 'wc_child_has_dimensions_' . $this->get_id(); + $has_dimension = get_transient( $transient_name ); + + if ( false === $has_dimension ) { + $has_dimension = $this->data_store->child_has_dimensions( $this ); + set_transient( $transient_name, $has_dimension, DAY_IN_SECONDS * 30 ); + } + return (bool) $has_dimension; + } + + /** + * Returns whether or not the product has dimensions set. + * + * @return bool + */ + public function has_dimensions() { + return parent::has_dimensions() || $this->child_has_dimensions(); + } + + /** + * Returns whether or not the product has weight set. + * + * @return bool + */ + public function has_weight() { + return parent::has_weight() || $this->child_has_weight(); + } + + /** + * Returns whether or not the product has additonal options that need + * selecting before adding to cart. + * + * @since 3.0.0 + * @return boolean + */ + public function has_options() { + return true; + } + + /* + |-------------------------------------------------------------------------- + | Sync with child variations. + |-------------------------------------------------------------------------- + */ + + /** + * Sync a variable product with it's children. These sync functions sync + * upwards (from child to parent) when the variation is saved. + * + * @param WC_Product|int $product Product object or ID for which you wish to sync. + * @param bool $save If true, the prouduct object will be saved to the DB before returning it. + * @return WC_Product Synced product object. + */ + public static function sync( $product, $save = true ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + if ( is_a( $product, 'WC_Product_Variable' ) ) { + $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); + $data_store->sync_price( $product ); + $data_store->sync_stock_status( $product ); + self::sync_attributes( $product ); // Legacy update of attributes. + + do_action( 'woocommerce_variable_product_sync_data', $product ); + + if ( $save ) { + $product->save(); + } + + wc_do_deprecated_action( 'woocommerce_variable_product_sync', array( $product->get_id(), $product->get_visible_children() ), '3.0', 'woocommerce_variable_product_sync_data, woocommerce_new_product or woocommerce_update_product' ); + } + return $product; + } + + /** + * Sync parent stock status with the status of all children and save. + * + * @param WC_Product|int $product Product object or ID for which you wish to sync. + * @param bool $save If true, the prouduct object will be saved to the DB before returning it. + * @return WC_Product Synced product object. + */ + public static function sync_stock_status( $product, $save = true ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + if ( is_a( $product, 'WC_Product_Variable' ) ) { + $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); + $data_store->sync_stock_status( $product ); + + if ( $save ) { + $product->save(); + } + } + return $product; + } + + /** + * Sort an associativate array of $variation_id => $price pairs in order of min and max prices. + * + * @param array $prices Associativate array of $variation_id => $price pairs + * @return array + */ + protected function sort_variation_prices( $prices ) { + asort( $prices ); + return $prices; + } } diff --git a/includes/class-wc-product-variation.php b/includes/class-wc-product-variation.php index b20b9e1a416..f0c58de6f0a 100644 --- a/includes/class-wc-product-variation.php +++ b/includes/class-wc-product-variation.php @@ -4,570 +4,485 @@ if ( ! defined( 'ABSPATH' ) ) { } /** - * Product Variation Class + * Product Variation Class. * * The WooCommerce product variation class handles product variation data. * - * @class WC_Product_Variation - * @version 2.2.0 - * @package WooCommerce/Classes - * @category Class - * @author WooThemes + * @class WC_Product_Variation + * @version 3.0.0 + * @package WooCommerce/Classes + * @category Class + * @author WooThemes */ -class WC_Product_Variation extends WC_Product { +class WC_Product_Variation extends WC_Product_Simple { - /** @public int ID of the variable product. */ - public $variation_id; + /** + * Post type. + * @var string + */ + public $post_type = 'product_variation'; - /** @public object Parent Variable product object. */ - public $parent; - - /** @public string Stores the shipping class of the variation. */ - public $variation_shipping_class = false; - - /** @public int Stores the shipping class ID of the variation. */ - public $variation_shipping_class_id = false; - - /** @public unused vars @deprecated in 2.2 */ - public $variation_has_sku = true; - public $variation_has_length = true; - public $variation_has_width = true; - public $variation_has_height = true; - public $variation_has_weight = true; - public $variation_has_tax_class = true; - public $variation_has_downloadable_files = true; - - /** @private array List of meta keys which apply to variations */ - public $variation_level_meta_keys = array( - 'manage_stock', - 'stock_status', - 'stock', - 'backorders', - 'sku', - 'downloadable_files', - 'weight', - 'length', - 'height', - 'downloadable', - 'virtual', - 'tax_class', - 'sale_price_dates_from', - 'sale_price_dates_to', - 'price', - 'regular_price', - 'sale_price' + /** + * Parent data. + * @var array + */ + protected $parent_data = array( + 'title' => '', + 'sku' => '', + 'manage_stock' => '', + 'backorders' => '', + 'stock_quantity' => '', + 'weight' => '', + 'length' => '', + 'width' => '', + 'height' => '', + 'tax_class' => '', + 'shipping_class_id' => '', + 'image_id' => '', + 'purchase_note' => '', ); /** - * Loads required variation data. + * Prefix for action and filter hooks on data. * - * @access public - * @param int $variation_id ID of the variation to load - * @param array $args Array of the arguments containing parent product data + * @since 3.0.0 + * @return string */ - public function __construct( $variation, $args = array() ) { - if ( is_object( $variation ) ) { - $this->variation_id = absint( $variation->ID ); - } else { - $this->variation_id = absint( $variation ); - } - - /* Get main product data from parent (args) */ - $this->id = ! empty( $args['parent_id'] ) ? intval( $args['parent_id'] ) : wp_get_post_parent_id( $this->variation_id ); - - // The post doesn't have a parent id, therefore its invalid. - if ( empty( $this->id ) ) { - return; - } - - $this->product_type = 'variation'; - $this->parent = ! empty( $args['parent'] ) ? $args['parent'] : get_product( $this->id ); - $this->post = ! empty( $this->parent->post ) ? $this->parent->post : array(); + protected function get_hook_prefix() { + return 'woocommerce_product_variation_get_'; } /** - * __isset function. - * - * @access public - * @param mixed $key - * @return bool + * Get internal type. + * @return string */ - public function __isset( $key ) { - if ( in_array( $key, $this->variation_level_meta_keys ) ) { - return metadata_exists( 'post', $this->variation_id, '_' . $key ) || metadata_exists( 'post', $this->id, '_' . $key ); - } else { - return metadata_exists( 'post', $this->id, '_' . $key ); - } + public function get_type() { + return 'variation'; } /** - * Get method returns variation meta data if set, otherwise in most cases the data from the parent. - * - * @access public - * @param string $key - * @return mixed + * If the stock level comes from another product ID. + * @since 3.0.0 + * @return int */ - public function __get( $key ) { - if ( in_array( $key, $this->variation_level_meta_keys ) ) { + public function get_stock_managed_by_id() { + return 'parent' === $this->get_manage_stock() ? $this->get_parent_id() : $this->get_id(); + } - // Get values or default if not set (no) - if ( in_array( $key, array( 'downloadable', 'virtual', 'manage_stock' ) ) ) { - $value = ( $value = get_post_meta( $this->variation_id, '_' . $key, true ) ) ? $value : 'no'; - - // Data which must be set (not null), otherwise use parent data - } elseif ( in_array( $key , array( 'tax_class', 'backorders' ) ) ) { - $value = metadata_exists( 'post', $this->variation_id, '_' . $key ) ? get_post_meta( $this->variation, '_' . $key, true ) : get_post_meta( $this->id, '_' . $key, true ); - - // Data which is only at variation level - no inheritance - } elseif ( in_array( $key , array( 'price', 'regular_price', 'sale_price', 'sale_price_dates_to', 'sale_price_dates_from' ) ) ) { - $value = ( $value = get_post_meta( $this->variation_id, '_' . $key, true ) ) ? $value : ''; - - } elseif ( 'stock' === $key ) { - $value = ( $value = get_post_meta( $this->variation_id, '_stock', true ) ) ? $value : 0; - - } else { - $value = ( $value = get_post_meta( $this->variation_id, '_' . $key, true ) ) ? $value : get_post_meta( $this->id, '_' . $key, true ); - } - - } elseif ( 'variation_data' === $key ) { - $all_meta = get_post_meta( $this->variation_id ); - - // Get the variation attributes from meta - foreach ( $all_meta as $name => $value ) { - if ( ! strstr( $name, 'attribute_' ) ) { - continue; - } - $this->variation_data[ $name ] = sanitize_title( $value[0] ); - } - return $this->variation_data; - - } elseif ( 'variation_has_stock' === $key ) { - return $this->managing_stock(); + /** + * Get the product's title. For variations this is the parent product name. + * + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_product_title', $this->parent_data['title'], $this ); + } + /** + * Get product name with SKU or ID. Used within admin. + * + * @return string Formatted product name + */ + public function get_formatted_name() { + if ( $this->get_sku() ) { + $identifier = $this->get_sku(); } else { - $value = parent::__get( $key ); + $identifier = '#' . $this->get_id(); + } + + $formatted_variation_list = wc_get_formatted_variation( $this, true, true ); + + return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ) . '' . $formatted_variation_list . ''; + } + + /** + * Get variation attribute values. Keys are prefixed with attribute_, as stored. + * + * @return array of attributes and their values for this variation + */ + public function get_variation_attributes() { + $attributes = $this->get_attributes(); + $variation_attributes = array(); + foreach ( $attributes as $key => $value ) { + $variation_attributes[ 'attribute_' . $key ] = $value; + } + return $variation_attributes; + } + + /** + * Returns a single product attribute as a string. + * @param string $attribute to get. + * @return string + */ + public function get_attribute( $attribute ) { + $attributes = $this->get_attributes(); + $attribute = sanitize_title( $attribute ); + + if ( isset( $attributes[ $attribute ] ) ) { + $value = $attributes[ $attribute ]; + $term = taxonomy_exists( $attribute ) ? get_term_by( 'slug', $value, $attribute ) : false; + $value = ! is_wp_error( $term ) && $term ? $term->name : $value; + } elseif ( isset( $attributes[ 'pa_' . $attribute ] ) ) { + $value = $attributes[ 'pa_' . $attribute ]; + $term = taxonomy_exists( 'pa_' . $attribute ) ? get_term_by( 'slug', $value, 'pa_' . $attribute ) : false; + $value = ! is_wp_error( $term ) && $term ? $term->name : $value; + } else { + return ''; } return $value; } - /** - * Returns whether or not the product post exists. - * - * @access public - * @return bool - */ - public function exists() { - return ! empty( $this->id ); - } - /** * Wrapper for get_permalink. Adds this variations attributes to the URL. + * + * @param $item_object item array If a cart or order item is passed, we can get a link containing the exact attributes selected for the variation, rather than the default attributes. * @return string */ - public function get_permalink() { - return add_query_arg( $this->variation_data, get_permalink( $this->id ) ); + public function get_permalink( $item_object = null ) { + $url = get_permalink( $this->get_parent_id() ); + + if ( ! empty( $item_object['variation'] ) ) { + $data = $item_object['variation']; + } elseif ( ! empty( $item_object['item_meta_array'] ) ) { + $data_keys = array_map( 'wc_variation_attribute_name', wp_list_pluck( $item_object['item_meta_array'], 'key' ) ); + $data_values = wp_list_pluck( $item_object['item_meta_array'], 'value' ); + $data = array_intersect_key( array_combine( $data_keys, $data_values ), $this->get_variation_attributes() ); + } else { + $data = $this->get_variation_attributes(); + } + + return add_query_arg( array_map( 'urlencode', array_filter( $data ) ), $url ); } /** * Get the add to url used mainly in loops. * - * @access public * @return string */ public function add_to_cart_url() { - $url = $this->is_purchasable() && $this->is_in_stock() ? remove_query_arg( 'added-to-cart', add_query_arg( array_merge( array( 'variation_id' => $this->variation_id, 'add-to-cart' => $this->id ), $this->variation_data ) ) ) : get_permalink( $this->id ); - + $url = $this->is_purchasable() ? remove_query_arg( 'added-to-cart', add_query_arg( array( 'variation_id' => $this->get_id(), 'add-to-cart' => $this->get_parent_id() ), $this->get_permalink() ) ) : $this->get_permalink(); return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this ); } /** - * Get the add to cart button text + * Get SKU (Stock-keeping unit) - product unique ID. * - * @access public + * @param string $context * @return string */ - public function add_to_cart_text() { - $text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add to cart', 'woocommerce' ) : __( 'Read More', 'woocommerce' ); + public function get_sku( $context = 'view' ) { + $value = $this->get_prop( 'sku', $context ); - return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this ); + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'sku', $this->parent_data['sku'], $this ); + } + return $value; } /** - * Checks if this particular variation is visible (variations with no price, or out of stock, can be hidden) + * Returns the product's weight. + * + * @param string $context + * @return string + */ + public function get_weight( $context = 'view' ) { + $value = $this->get_prop( 'weight', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'weight', $this->parent_data['weight'], $this ); + } + return $value; + } + + /** + * Returns the product length. + * + * @param string $context + * @return string + */ + public function get_length( $context = 'view' ) { + $value = $this->get_prop( 'length', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'length', $this->parent_data['length'], $this ); + } + return $value; + } + + /** + * Returns the product width. + * + * @param string $context + * @return string + */ + public function get_width( $context = 'view' ) { + $value = $this->get_prop( 'width', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'width', $this->parent_data['width'], $this ); + } + return $value; + } + + /** + * Returns the product height. + * + * @param string $context + * @return string + */ + public function get_height( $context = 'view' ) { + $value = $this->get_prop( 'height', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'height', $this->parent_data['height'], $this ); + } + return $value; + } + + /** + * Returns the tax class. + * + * Does not use get_prop so it can handle 'parent' Inheritance correctly. + * + * @param string $context view, edit, or unfiltered + * @return string + */ + public function get_tax_class( $context = 'view' ) { + $value = null; + + if ( array_key_exists( 'tax_class', $this->data ) ) { + $value = array_key_exists( 'tax_class', $this->changes ) ? $this->changes['tax_class'] : $this->data['tax_class']; + + if ( 'edit' !== $context && 'parent' === $value ) { + $value = $this->parent_data['tax_class']; + } + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . 'tax_class', $value, $this ); + } + } + return $value; + } + + /** + * Return if product manage stock. + * + * @since 3.0.0 + * @param string $context + * @return boolean|string true, false, or parent. + */ + public function get_manage_stock( $context = 'view' ) { + $value = $this->get_prop( 'manage_stock', $context ); + + // Inherit value from parent. + if ( 'view' === $context && false === $value && true === wc_string_to_bool( $this->parent_data['manage_stock'] ) ) { + $value = 'parent'; + } + return $value; + } + + /** + * Returns number of items available for sale. + * + * @param string $context + * @return int|null + */ + public function get_stock_quantity( $context = 'view' ) { + $value = $this->get_prop( 'stock_quantity', $context ); + + // Inherit value from parent. + if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { + $value = apply_filters( $this->get_hook_prefix() . 'stock_quantity', $this->parent_data['stock_quantity'], $this ); + } + return $value; + } + + /** + * Get backorders. + * + * @param string $context + * @since 3.0.0 + * @return string yes no or notify + */ + public function get_backorders( $context = 'view' ) { + $value = $this->get_prop( 'backorders', $context ); + + // Inherit value from parent. + if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { + $value = apply_filters( $this->get_hook_prefix() . 'backorders', $this->parent_data['backorders'], $this ); + } + return $value; + } + + /** + * Get main image ID. + * + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_image_id( $context = 'view' ) { + $image_id = $this->get_prop( 'image_id', $context ); + + if ( 'view' === $context && ! $image_id ) { + $image_id = apply_filters( $this->get_hook_prefix() . 'image_id', $this->parent_data['image_id'], $this ); + } + + return $image_id; + } + + /** + * Get purchase note. + * + * @since 3.0.0 + * @param string $context + * @return string + */ + public function get_purchase_note( $context = 'view' ) { + $value = $this->get_prop( 'purchase_note', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'purchase_note', $this->parent_data['purchase_note'], $this ); + } + return $value; + } + + /** + * Get shipping class ID. + * + * @since 3.0.0 + * @param string $context + * @return int + */ + public function get_shipping_class_id( $context = 'view' ) { + $shipping_class_id = $this->get_prop( 'shipping_class_id', $context ); + + if ( 'view' === $context && ! $shipping_class_id ) { + $shipping_class_id = apply_filters( $this->get_hook_prefix() . 'shipping_class_id', $this->parent_data['shipping_class_id'], $this ); + } + + return $shipping_class_id; + } + + /** + * Get catalog visibility. + * + * @param string $context + * @return string + */ + public function get_catalog_visibility( $context = 'view' ) { + return apply_filters( $this->get_hook_prefix() . 'catalog_visibility', $this->parent_data['catalog_visibility'], $this ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + */ + + /** + * Set the parent data array for this variation. + * + * @since 3.0.0 + * @param array + */ + public function set_parent_data( $parent_data ) { + $this->parent_data = $parent_data; + } + + /** + * Get the parent data array for this variation. + * + * @since 3.0.0 + * @return array + */ + public function get_parent_data() { + return $this->parent_data; + } + + /** + * Set attributes. Unlike the parent product which uses terms, variations are assigned + * specific attributes using name value pairs. + * @param array $raw_attributes + */ + public function set_attributes( $raw_attributes ) { + $raw_attributes = (array) $raw_attributes; + $attributes = array(); + + foreach ( $raw_attributes as $key => $value ) { + // Remove attribute prefix which meta gets stored with. + if ( 0 === strpos( $key, 'attribute_' ) ) { + $key = substr( $key, 10 ); + } + $attributes[ $key ] = $value; + } + $this->set_prop( 'attributes', $attributes ); + } + + /** + * Returns array of attribute name value pairs. Keys are prefixed with attribute_, as stored. + * + * @param string $context + * @return array + */ + public function get_attributes( $context = 'view' ) { + return $this->get_prop( 'attributes', $context ); + } + + /** + * Returns whether or not the product has any visible attributes. + * + * Variations are mapped to specific attributes unlike products, and the return + * value of ->get_attributes differs. Therefore this returns false. + * + * @return boolean + */ + public function has_attributes() { + return false; + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + */ + + /** + * Returns false if the product cannot be bought. + * Override abstract method so that: i) Disabled variations are not be purchasable by admins. ii) Enabled variations are not purchasable if the parent product is not purchasable. + * + * @return bool + */ + public function is_purchasable() { + return apply_filters( 'woocommerce_variation_is_purchasable', $this->variation_is_visible() && parent::is_purchasable(), $this ); + } + + /** + * Controls whether this particular variation will appear greyed-out (inactive) or not (active). + * Used by extensions to make incompatible variations appear greyed-out, etc. + * Other possible uses: prevent out-of-stock variations from being selected. + * + * @return bool + */ + public function variation_is_active() { + return apply_filters( 'woocommerce_variation_is_active', true, $this ); + } + + /** + * Checks if this particular variation is visible. Invisible variations are enabled and can be selected, but no price / stock info is displayed. + * Instead, a suitable 'unavailable' message is displayed. + * Invisible by default: Disabled variations and variations with an empty price. * * @return bool */ public function variation_is_visible() { - $visible = true; - - // Published == enabled checkbox - if ( get_post_status( $this->variation_id ) != 'publish' ) { - $visible = false; - } - - // Out of stock visibility - elseif ( get_option('woocommerce_hide_out_of_stock_items') == 'yes' && ! $this->is_in_stock() ) { - $visible = false; - } - - // Price not set - elseif ( $this->get_price() === "" ) { - $visible = false; - } - - return apply_filters( 'woocommerce_variation_is_visible', $visible, $this->variation_id, $this->id ); - } - - /** - * Returns false if the product cannot be bought. - * - * @access public - * @return bool - */ - public function is_purchasable() { - // Published == enabled checkbox - if ( get_post_status( $this->variation_id ) != 'publish' ) { - $purchasable = false; - } else { - $purchasable = parent::is_purchasable(); - } - return apply_filters( 'woocommerce_variation_is_purchasable', $purchasable, $this ); - } - - /** - * Returns whether or not the variations parent is visible. - * - * @access public - * @return bool - */ - public function parent_is_visible() { - return $this->is_visible(); - } - - /** - * Get variation ID - * - * @return int - */ - public function get_variation_id() { - return absint( $this->variation_id ); - } - - /** - * Get variation attribute values - * - * @return array of attributes and their values for this variation - */ - public function get_variation_attributes() { - return $this->variation_data; - } - - /** - * Get variation price HTML. Prices are not inherited from parents. - * - * @return string containing the formatted price - */ - public function get_price_html( $price = '' ) { - $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); - $display_price = $tax_display_mode == 'incl' ? $this->get_price_including_tax() : $this->get_price_excluding_tax(); - $display_regular_price = $tax_display_mode == 'incl' ? $this->get_price_including_tax( 1, $this->get_regular_price() ) : $this->get_price_excluding_tax( 1, $this->get_regular_price() ); - $display_sale_price = $tax_display_mode == 'incl' ? $this->get_price_including_tax( 1, $this->get_sale_price() ) : $this->get_price_excluding_tax( 1, $this->get_sale_price() ); - - if ( $this->get_price() !== '' ) { - if ( $this->is_on_sale() ) { - $price = apply_filters( 'woocommerce_variation_sale_price_html', '' . wc_price( $display_regular_price ) . ' ' . wc_price( $display_sale_price ) . '' . $this->get_price_suffix(), $this ); - } elseif ( $this->get_price() > 0 ) { - $price = apply_filters( 'woocommerce_variation_price_html', wc_price( $display_price ) . $this->get_price_suffix(), $this ); - } else { - $price = apply_filters( 'woocommerce_variation_free_price_html', __( 'Free!', 'woocommerce' ), $this ); - } - } else { - $price = apply_filters( 'woocommerce_variation_empty_price_html', '', $this ); - } - - return apply_filters( 'woocommerce_get_variation_price_html', $price, $this ); - } - - /** - * Gets the main product image ID. - * @return int - */ - public function get_image_id() { - if ( $this->variation_id && has_post_thumbnail( $this->variation_id ) ) { - $image_id = get_post_thumbnail_id( $this->variation_id ); - } elseif ( has_post_thumbnail( $this->id ) ) { - $image_id = get_post_thumbnail_id( $this->id ); - } elseif ( ( $parent_id = wp_get_post_parent_id( $this->id ) ) && has_post_thumbnail( $parent_id ) ) { - $image_id = get_post_thumbnail_id( $parent_id ); - } else { - $image_id = 0; - } - return $image_id; - } - - /** - * Gets the main product image. - * - * @access public - * @param string $size (default: 'shop_thumbnail') - * @return string - */ - public function get_image( $size = 'shop_thumbnail', $attr = array() ) { - if ( $this->variation_id && has_post_thumbnail( $this->variation_id ) ) { - $image = get_the_post_thumbnail( $this->variation_id, $size, $attr ); - } elseif ( has_post_thumbnail( $this->id ) ) { - $image = get_the_post_thumbnail( $this->id, $size, $attr ); - } elseif ( ( $parent_id = wp_get_post_parent_id( $this->id ) ) && has_post_thumbnail( $parent_id ) ) { - $image = get_the_post_thumbnail( $parent_id, $size , $attr); - } else { - $image = wc_placeholder_img( $size ); - } - return $image; - } - - /** - * Returns whether or not the product is in stock. - * - * @access public - * @return bool - */ - public function is_in_stock() { - // If we're managing stock at variation level, check stock levels - if ( $this->managing_stock() ) { - if ( $this->backorders_allowed() ) { - return true; - } elseif ( $this->get_total_stock() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) { - return false; - } else { - return $this->stock_status === 'instock'; - } - } - else { - return $this->stock_status === 'instock'; - } - } - - /** - * Set stock level of the product variation. - * - * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). - * We cannot rely on the original loaded value in case another order was made since then. - * - * @param int $amount - * @param string $mode can be set, add, or subtract - * @return int new stock level - */ - public function set_stock( $amount = null, $mode = 'set' ) { - global $wpdb; - - if ( ! is_null( $amount ) && $this->managing_stock() ) { - - // Ensure key exists - add_post_meta( $this->variation_id, '_stock', 0, true ); - - // Update stock in DB directly - switch ( $mode ) { - case 'add' : - $wpdb->query( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + {$amount} WHERE post_id = {$this->variation_id} AND meta_key='_stock'" ); - break; - case 'subtract' : - $wpdb->query( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - {$amount} WHERE post_id = {$this->variation_id} AND meta_key='_stock'" ); - break; - default : - $wpdb->query( "UPDATE {$wpdb->postmeta} SET meta_value = {$amount} WHERE post_id = {$this->variation_id} AND meta_key='_stock'" ); - break; - } - - // Clear caches - wp_cache_delete( $this->variation_id, 'post_meta' ); - - // Clear total stock transient - delete_transient( 'wc_product_total_stock_' . $this->id ); - - // Stock status - $this->check_stock_status(); - - // Sync the parent - WC_Product_Variable::sync( $this->id ); - - // Trigger action - do_action( 'woocommerce_variation_set_stock', $this ); - - } elseif ( ! is_null( $amount ) ) { - return $this->parent->set_stock( $amount, $mode ); - } - - return $this->get_stock_quantity(); - } - - /** - * set_stock_status function. - * - * @access public - */ - public function set_stock_status( $status ) { - $status = 'outofstock' === $status ? 'outofstock' : 'instock'; - - // Sanity check - if ( $this->managing_stock() ) { - if ( ! $this->backorders_allowed() && $this->get_stock_quantity() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) { - $status = 'outofstock'; - } - } elseif ( $this->parent->managing_stock() ) { - if ( ! $this->parent->backorders_allowed() && $this->parent->get_stock_quantity() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) { - $status = 'outofstock'; - } - } - - if ( update_post_meta( $this->variation_id, '_stock_status', $status ) ) { - do_action( 'woocommerce_variation_set_stock_status', $this->variation_id, $status ); - - if ( $this->managing_stock() ) { - WC_Product_Variable::sync_stock_status( $this->id ); - } - } - } - - /** - * Reduce stock level of the product. - * - * @param int $amount (default: 1) Amount to reduce by - * @return int stock level - */ - public function reduce_stock( $amount = 1 ) { - if ( $this->managing_stock() ) { - return $this->set_stock( $amount, 'subtract' ); - } else { - return $this->parent->reduce_stock( $amount ); - } - } - - /** - * Increase stock level of the product. - * - * @param int $amount (default: 1) Amount to increase by - * @return int stock level - */ - public function increase_stock( $amount = 1 ) { - if ( $this->managing_stock() ) { - return $this->set_stock( $amount, 'add' ); - } else { - return $this->parent->increase_stock( $amount ); - } - } - - /** - * Returns the availability of the product. - * - * @access public - * @return string - */ - public function get_availability() { - if ( $this->managing_stock() ) { - return parent::get_availability(); - } else { - return $this->parent->get_availability(); - } - } - - /** - * Returns whether or not the product needs to notify the customer on backorder. - * - * @access public - * @return bool - */ - public function backorders_require_notification() { - if ( $this->managing_stock() ) { - return parent::backorders_require_notification(); - } else { - return $this->parent->backorders_require_notification(); - } - } - - /** - * is_on_backorder function. - * - * @access public - * @param int $qty_in_cart (default: 0) - * @return bool - */ - public function is_on_backorder( $qty_in_cart = 0 ) { - if ( $this->managing_stock() ) { - return parent::is_on_backorder( $qty_in_cart ); - } else { - return $this->parent->is_on_backorder( $qty_in_cart ); - } } - - /** - * Returns whether or not the product has enough stock for the order. - * - * @access public - * @param mixed $quantity - * @return bool - */ - public function has_enough_stock( $quantity ) { - if ( $this->managing_stock() ) { - return parent::has_enough_stock( $quantity ); - } else { - return $this->parent->has_enough_stock( $quantity ); - } } - - /** - * Get the shipping class, and if not set, get the shipping class of the parent. - * - * @access public - * @return string - */ - public function get_shipping_class() { - if ( ! $this->variation_shipping_class ) { - $classes = get_the_terms( $this->variation_id, 'product_shipping_class' ); - - if ( $classes && ! is_wp_error( $classes ) ) { - $this->variation_shipping_class = current( $classes )->slug; - } else { - $this->variation_shipping_class = parent::get_shipping_class(); - } - } - return $this->variation_shipping_class; - } - - /** - * Returns the product shipping class ID. - * - * @access public - * @return int - */ - public function get_shipping_class_id() { - if ( ! $this->variation_shipping_class_id ) { - $classes = get_the_terms( $this->variation_id, 'product_shipping_class' ); - - if ( $classes && ! is_wp_error( $classes ) ) { - $this->variation_shipping_class_id = current( $classes )->term_id; - } else { - $this->variation_shipping_class_id = parent::get_shipping_class_id(); - } - } - return absint( $this->variation_shipping_class_id ); - } - - /** - * Get product name with extra details such as SKU, price and attributes. Used within admin. - * - * @access public - * @param mixed $product - * @return string Formatted product name, including attributes and price - */ - public function get_formatted_name() { - if ( $this->get_sku() ) { - $identifier = $this->get_sku(); - } else { - $identifier = '#' . $this->variation_id; - } - - $attributes = $this->get_variation_attributes(); - $extra_data = ' – ' . implode( ', ', $attributes ) . ' – ' . wc_price( $this->get_price() ); - - return sprintf( __( '%s – %s%s', 'woocommerce' ), $identifier, $this->get_title(), $extra_data ); + return apply_filters( 'woocommerce_variation_is_visible', 'publish' === get_post_status( $this->get_id() ) && '' !== $this->get_price(), $this->get_id(), $this->get_parent_id(), $this ); } } diff --git a/includes/class-wc-query.php b/includes/class-wc-query.php index a902442eb3c..d8065343756 100644 --- a/includes/class-wc-query.php +++ b/includes/class-wc-query.php @@ -1,46 +1,31 @@ init_query_vars(); } + /** + * Get any errors from querystring. + */ + public function get_errors() { + if ( ! empty( $_GET['wc_error'] ) && ( $error = sanitize_text_field( $_GET['wc_error'] ) ) && ! wc_has_notice( $error, 'error' ) ) { + wc_add_notice( $error, 'error' ); + } + } + /** * Init query vars by loading options. */ public function init_query_vars() { - // Query vars to add to WP + // Query vars to add to WP. $this->query_vars = array( - // Checkout actions + // Checkout actions. 'order-pay' => get_option( 'woocommerce_checkout_pay_endpoint', 'order-pay' ), 'order-received' => get_option( 'woocommerce_checkout_order_received_endpoint', 'order-received' ), - - // My account actions - 'view-order' => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ), - 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), - 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), - 'lost-password' => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ), - 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), - 'add-payment-method' => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ), + // My account actions. + 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), + 'view-order' => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ), + 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), + 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), + 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), + 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), + 'lost-password' => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ), + 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), + 'add-payment-method' => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ), + 'delete-payment-method' => get_option( 'woocommerce_myaccount_delete_payment_method_endpoint', 'delete-payment-method' ), + 'set-default-payment-method' => get_option( 'woocommerce_myaccount_set_default_payment_method_endpoint', 'set-default-payment-method' ), ); } /** - * Get any errors from querystring + * Get page title for an endpoint. + * @param string + * @return string */ - public function get_errors() { - if ( ! empty( $_GET['wc_error'] ) && ( $error = sanitize_text_field( $_GET['wc_error'] ) ) && ! wc_has_notice( $error, 'error' ) ) - wc_add_notice( $error, 'error' ); + public function get_endpoint_title( $endpoint ) { + global $wp; + + switch ( $endpoint ) { + case 'order-pay' : + $title = __( 'Pay for order', 'woocommerce' ); + break; + case 'order-received' : + $title = __( 'Order received', 'woocommerce' ); + break; + case 'orders' : + if ( ! empty( $wp->query_vars['orders'] ) ) { + /* translators: %s: page */ + $title = sprintf( __( 'Orders (page %d)', 'woocommerce' ), intval( $wp->query_vars['orders'] ) ); + } else { + $title = __( 'Orders', 'woocommerce' ); + } + break; + case 'view-order' : + $order = wc_get_order( $wp->query_vars['view-order'] ); + /* translators: %s: order number */ + $title = ( $order ) ? sprintf( __( 'Order #%s', 'woocommerce' ), $order->get_order_number() ) : ''; + break; + case 'downloads' : + $title = __( 'Downloads', 'woocommerce' ); + break; + case 'edit-account' : + $title = __( 'Account details', 'woocommerce' ); + break; + case 'edit-address' : + $title = __( 'Addresses', 'woocommerce' ); + break; + case 'payment-methods' : + $title = __( 'Payment methods', 'woocommerce' ); + break; + case 'add-payment-method' : + $title = __( 'Add payment method', 'woocommerce' ); + break; + case 'lost-password' : + $title = __( 'Lost password', 'woocommerce' ); + break; + default : + $title = ''; + break; + } + + return apply_filters( 'woocommerce_endpoint_' . $endpoint . '_title', $title, $endpoint ); } /** - * Add endpoints for query vars + * Endpoint mask describing the places the endpoint should be added. + * + * @since 2.6.2 + * @return int + */ + public function get_endpoints_mask() { + if ( 'page' === get_option( 'show_on_front' ) ) { + $page_on_front = get_option( 'page_on_front' ); + $myaccount_page_id = get_option( 'woocommerce_myaccount_page_id' ); + $checkout_page_id = get_option( 'woocommerce_checkout_page_id' ); + + if ( in_array( $page_on_front, array( $myaccount_page_id, $checkout_page_id ) ) ) { + return EP_ROOT | EP_PAGES; + } + } + + return EP_PAGES; + } + + /** + * Add endpoints for query vars. */ public function add_endpoints() { - foreach ( $this->query_vars as $key => $var ) - add_rewrite_endpoint( $var, EP_ROOT | EP_PAGES ); + $mask = $this->get_endpoints_mask(); + + foreach ( $this->query_vars as $key => $var ) { + if ( ! empty( $var ) ) { + add_rewrite_endpoint( $var, $mask ); + } + } } /** - * add_query_vars function. + * Add query vars. * * @access public * @param array $vars * @return array */ public function add_query_vars( $vars ) { - foreach ( $this->query_vars as $key => $var ) + foreach ( $this->get_query_vars() as $key => $var ) { $vars[] = $key; - + } return $vars; } /** - * Get query vars - * @return array() + * Get query vars. + * + * @return array */ public function get_query_vars() { - return $this->query_vars; + return apply_filters( 'woocommerce_get_query_vars', $this->query_vars ); } /** - * Parse the request and look for query vars - endpoints may not be supported + * Get query current active query var. + * + * @return string + */ + public function get_current_endpoint() { + global $wp; + foreach ( $this->get_query_vars() as $key => $value ) { + if ( isset( $wp->query_vars[ $key ] ) ) { + return $key; + } + } + return ''; + } + + /** + * Parse the request and look for query vars - endpoints may not be supported. */ public function parse_request() { global $wp; // Map query vars to their keys, or get them if endpoints are not supported - foreach ( $this->query_vars as $key => $var ) { + foreach ( $this->get_query_vars() as $key => $var ) { if ( isset( $_GET[ $var ] ) ) { $wp->query_vars[ $key ] = $_GET[ $var ]; - } - - elseif ( isset( $wp->query_vars[ $var ] ) ) { + } elseif ( isset( $wp->query_vars[ $var ] ) ) { $wp->query_vars[ $key ] = $wp->query_vars[ $var ]; } } } /** - * Hook into pre_get_posts to do the main product query + * Are we currently on the front page? * - * @access public - * @param mixed $q query object - * @return void + * @param object $q + * + * @return bool + */ + private function is_showing_page_on_front( $q ) { + return $q->is_home() && 'page' === get_option( 'show_on_front' ); + } + + /** + * Is the front page a page we define? + * + * @param int $page_id + * + * @return bool + */ + private function page_on_front_is( $page_id ) { + return absint( get_option( 'page_on_front' ) ) === absint( $page_id ); + } + + /** + * Hook into pre_get_posts to do the main product query. + * + * @param object $q query object */ public function pre_get_posts( $q ) { // We only want to affect the main query @@ -154,60 +252,54 @@ class WC_Query { return; } - // Fix for verbose page rules - if ( $GLOBALS['wp_rewrite']->use_verbose_page_rules && isset( $q->queried_object_id ) && $q->queried_object_id === wc_get_page_id('shop') ) { - $q->set( 'post_type', 'product' ); - $q->set( 'page', '' ); - $q->set( 'pagename', '' ); - - // Fix conditional Functions - $q->is_archive = true; - $q->is_post_type_archive = true; - $q->is_singular = false; - $q->is_page = false; - } - // Fix for endpoints on the homepage - if ( $q->is_home() && 'page' == get_option('show_on_front') && get_option('page_on_front') != $q->get('page_id') ) { + if ( $this->is_showing_page_on_front( $q ) && ! $this->page_on_front_is( $q->get( 'page_id' ) ) ) { $_query = wp_parse_args( $q->query ); if ( ! empty( $_query ) && array_intersect( array_keys( $_query ), array_keys( $this->query_vars ) ) ) { $q->is_page = true; $q->is_home = false; $q->is_singular = true; - - $q->set( 'page_id', get_option('page_on_front') ); + $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); + add_filter( 'redirect_canonical', '__return_false' ); } } - + // When orderby is set, WordPress shows posts. Get around that here. - if ( $q->is_home() && 'page' == get_option('show_on_front') && get_option('page_on_front') == wc_get_page_id('shop') ) { + if ( $this->is_showing_page_on_front( $q ) && $this->page_on_front_is( wc_get_page_id( 'shop' ) ) ) { $_query = wp_parse_args( $q->query ); if ( empty( $_query ) || ! array_diff( array_keys( $_query ), array( 'preview', 'page', 'paged', 'cpage', 'orderby' ) ) ) { $q->is_page = true; $q->is_home = false; - $q->set( 'page_id', get_option('page_on_front') ); + $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); $q->set( 'post_type', 'product' ); } } - // Special check for shops with the product archive on front - if ( $q->is_page() && 'page' == get_option( 'show_on_front' ) && $q->get('page_id') == wc_get_page_id('shop') ) { + // Fix product feeds + if ( $q->is_feed() && $q->is_post_type_archive( 'product' ) ) { + $q->is_comment_feed = false; + } + // Special check for shops with the product archive on front + if ( $q->is_page() && 'page' === get_option( 'show_on_front' ) && absint( $q->get( 'page_id' ) ) === wc_get_page_id( 'shop' ) ) { // This is a front-page shop $q->set( 'post_type', 'product' ); $q->set( 'page_id', '' ); + if ( isset( $q->query['paged'] ) ) { $q->set( 'paged', $q->query['paged'] ); } // Define a variable so we know this is the front page shop later on - define( 'SHOP_IS_ON_FRONT', true ); + if ( ! defined( 'SHOP_IS_ON_FRONT' ) ) { + define( 'SHOP_IS_ON_FRONT', true ); + } // Get the actual WP page to avoid errors and let us use is_front_page() - // This is hacky but works. Awaiting http://core.trac.wordpress.org/ticket/21096 + // This is hacky but works. Awaiting https://core.trac.wordpress.org/ticket/21096 global $wp_post_types; - $shop_page = get_post( wc_get_page_id('shop') ); + $shop_page = get_post( wc_get_page_id( 'shop' ) ); $wp_post_types['product']->ID = $shop_page->ID; $wp_post_types['product']->post_title = $shop_page->post_title; @@ -221,6 +313,9 @@ class WC_Query { $q->is_archive = true; $q->is_page = true; + // Remove post type archive name from front page title tag + add_filter( 'post_type_archive_title', '__return_empty_string', 5 ); + // Fix WP SEO if ( class_exists( 'WPSEO_Meta' ) ) { add_filter( 'wpseo_metadesc', array( $this, 'wpseo_metadesc' ) ); @@ -235,21 +330,16 @@ class WC_Query { $this->product_query( $q ); if ( is_search() ) { - add_filter( 'posts_where', array( $this, 'search_post_excerpt' ) ); - add_filter( 'wp', array( $this, 'remove_posts_where' ) ); + add_filter( 'posts_where', array( $this, 'search_post_excerpt' ) ); + add_filter( 'wp', array( $this, 'remove_posts_where' ) ); } - add_filter( 'posts_where', array( $this, 'exclude_protected_products' ) ); - - // We're on a shop page so queue the woocommerce_get_products_in_view function - add_action( 'wp', array( $this, 'get_products_in_view' ), 2); - // And remove the pre_get_posts hook $this->remove_product_query(); } /** - * search_post_excerpt function. + * Search post excerpt. * * @access public * @param string $where (default: '') @@ -259,250 +349,108 @@ class WC_Query { global $wp_the_query; // If this is not a WC Query, do not modify the query - if ( empty( $wp_the_query->query_vars['wc_query'] ) || empty( $wp_the_query->query_vars['s'] ) ) - return $where; + if ( empty( $wp_the_query->query_vars['wc_query'] ) || empty( $wp_the_query->query_vars['s'] ) ) { + return $where; + } $where = preg_replace( - "/post_title\s+LIKE\s*(\'\%[^\%]+\%\')/", - "post_title LIKE $1) OR (post_excerpt LIKE $1", $where ); + "/post_title\s+LIKE\s*(\'\%[^\%]+\%\')/", + "post_title LIKE $1) OR (post_excerpt LIKE $1", $where ); return $where; } /** - * Prevent password protected products appearing in the loops + * WP SEO meta description. * - * @param string $where - * @return string - */ - public function exclude_protected_products( $where ) { - global $wpdb; - $where .= " AND {$wpdb->posts}.post_password = ''"; - return $where; - } - - /** - * wpseo_metadesc function. - * Hooked into wpseo_ hook already, so no need for function_exist + * Hooked into wpseo_ hook already, so no need for function_exist. * * @access public * @return string */ public function wpseo_metadesc() { - return WPSEO_Meta::get_value( 'metadesc', wc_get_page_id('shop') ); - + return WPSEO_Meta::get_value( 'metadesc', wc_get_page_id( 'shop' ) ); } - /** - * wpseo_metakey function. - * Hooked into wpseo_ hook already, so no need for function_exist + * WP SEO meta key. + * + * Hooked into wpseo_ hook already, so no need for function_exist. * * @access public * @return string */ public function wpseo_metakey() { - return WPSEO_Meta::get_value( 'metakey', wc_get_page_id('shop') ); + return WPSEO_Meta::get_value( 'metakey', wc_get_page_id( 'shop' ) ); } - /** - * Hook into the_posts to do the main product query if needed - relevanssi compatibility + * Query the products, applying sorting/ordering etc. This applies to the main wordpress loop. * - * @access public - * @param array $posts - * @param WP_Query|bool $query (default: false) - * @return array - */ - public function the_posts( $posts, $query = false ) { - // Abort if there's no query - if ( ! $query ) - return $posts; - - // Abort if we're not filtering posts - if ( empty( $this->post__in ) ) - return $posts; - - // Abort if this query has already been done - if ( ! empty( $query->wc_query ) ) - return $posts; - - // Abort if this isn't a search query - if ( empty( $query->query_vars["s"] ) ) - return $posts; - - // Abort if we're not on a post type archive/product taxonomy - if ( ! $query->is_post_type_archive( 'product' ) && ! $query->is_tax( get_object_taxonomies( 'product' ) ) ) - return $posts; - - $filtered_posts = array(); - $queried_post_ids = array(); - - foreach ( $posts as $post ) { - if ( in_array( $post->ID, $this->post__in ) ) { - $filtered_posts[] = $post; - $queried_post_ids[] = $post->ID; - } - } - - $query->posts = $filtered_posts; - $query->post_count = count( $filtered_posts ); - - // Ensure filters are set - $this->unfiltered_product_ids = $queried_post_ids; - $this->filtered_product_ids = $queried_post_ids; - - if ( sizeof( $this->layered_nav_post__in ) > 0 ) { - $this->layered_nav_product_ids = array_intersect( $this->unfiltered_product_ids, $this->layered_nav_post__in ); - } else { - $this->layered_nav_product_ids = $this->unfiltered_product_ids; - } - - return $filtered_posts; - } - - - /** - * Query the products, applying sorting/ordering etc. This applies to the main wordpress loop - * - * @access public * @param mixed $q - * @return void */ public function product_query( $q ) { - - // Meta query - $meta_query = $this->get_meta_query( $q->get( 'meta_query' ) ); - - // Ordering - $ordering = $this->get_catalog_ordering_args(); - - // Get a list of post id's which match the current filters set (in the layered nav and price filter) - $post__in = array_unique( apply_filters( 'loop_shop_post_in', array() ) ); - // Ordering query vars - $q->set( 'orderby', $ordering['orderby'] ); - $q->set( 'order', $ordering['order'] ); - if ( isset( $ordering['meta_key'] ) ) - $q->set( 'meta_key', $ordering['meta_key'] ); + if ( ! $q->is_search() ) { + $ordering = $this->get_catalog_ordering_args(); + $q->set( 'orderby', $ordering['orderby'] ); + $q->set( 'order', $ordering['order'] ); + if ( isset( $ordering['meta_key'] ) ) { + $q->set( 'meta_key', $ordering['meta_key'] ); + } + } else { + $q->set( 'orderby', 'relevance' ); + } // Query vars that affect posts shown - $q->set( 'meta_query', $meta_query ); - $q->set( 'post__in', $post__in ); + $q->set( 'meta_query', $this->get_meta_query( $q->get( 'meta_query' ), true ) ); + $q->set( 'tax_query', $this->get_tax_query( $q->get( 'tax_query' ), true ) ); $q->set( 'posts_per_page', $q->get( 'posts_per_page' ) ? $q->get( 'posts_per_page' ) : apply_filters( 'loop_shop_per_page', get_option( 'posts_per_page' ) ) ); - - // Set a special variable - $q->set( 'wc_query', true ); - - // Store variables - $this->post__in = $post__in; - $this->meta_query = $meta_query; + $q->set( 'wc_query', 'product_query' ); + $q->set( 'post__in', array_unique( (array) apply_filters( 'loop_shop_post_in', array() ) ) ); do_action( 'woocommerce_product_query', $q, $this ); } /** - * Remove the query - * - * @access public - * @return void + * Remove the query. */ public function remove_product_query() { remove_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); } /** - * Remove ordering queries + * Remove ordering queries. */ public function remove_ordering_args() { + remove_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) ); + remove_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); remove_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); } /** - * Remove the posts_where filter - * - * @access public - * @return void + * Remove the posts_where filter. */ public function remove_posts_where() { remove_filter( 'posts_where', array( $this, 'search_post_excerpt' ) ); } - /** - * Get an unpaginated list all product ID's (both filtered and unfiltered). Makes use of transients. + * Returns an array of arguments for ordering products based on the selected values. * * @access public - * @return void - */ - public function get_products_in_view() { - global $wp_the_query; - - $unfiltered_product_ids = array(); - - // Get main query - $current_wp_query = $wp_the_query->query; - - // Get WP Query for current page (without 'paged') - unset( $current_wp_query['paged'] ); - - // Generate a transient name based on current query - $transient_name = 'wc_uf_pid_' . md5( http_build_query( $current_wp_query ) . WC_Cache_Helper::get_transient_version( 'product_query' ) ); - $transient_name = ( is_search() ) ? $transient_name . '_s' : $transient_name; - - if ( false === ( $unfiltered_product_ids = get_transient( $transient_name ) ) ) { - - // Get all visible posts, regardless of filters - $unfiltered_product_ids = get_posts( - array_merge( - $current_wp_query, - array( - 'post_type' => 'product', - 'numberposts' => -1, - 'post_status' => 'publish', - 'meta_query' => $this->meta_query, - 'fields' => 'ids', - 'no_found_rows' => true, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false - ) - ) - ); - - set_transient( $transient_name, $unfiltered_product_ids, YEAR_IN_SECONDS ); - } - - // Store the variable - $this->unfiltered_product_ids = $unfiltered_product_ids; - - // Also store filtered posts ids... - if ( sizeof( $this->post__in ) > 0 ) { - $this->filtered_product_ids = array_intersect( $this->unfiltered_product_ids, $this->post__in ); - } else { - $this->filtered_product_ids = $this->unfiltered_product_ids; - } - - // And filtered post ids which just take layered nav into consideration (to find max price in the price widget) - if ( sizeof( $this->layered_nav_post__in ) > 0 ) { - $this->layered_nav_product_ids = array_intersect( $this->unfiltered_product_ids, $this->layered_nav_post__in ); - } else { - $this->layered_nav_product_ids = $this->unfiltered_product_ids; - } - } - - - /** - * Returns an array of arguments for ordering products based on the selected values * - * @access public + * @param string $orderby + * @param string $order + * * @return array */ public function get_catalog_ordering_args( $orderby = '', $order = '' ) { // Get ordering from query string unless defined if ( ! $orderby ) { - $orderby_value = isset( $_GET['orderby'] ) ? wc_clean( $_GET['orderby'] ) : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby' ) ); + $orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) $_GET['orderby'] ) : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby' ) ); // Get order + orderby args from string $orderby_value = explode( '-', $orderby_value ); @@ -512,48 +460,80 @@ class WC_Query { $orderby = strtolower( $orderby ); $order = strtoupper( $order ); - - $args = array(); + $args = array(); // default - menu_order $args['orderby'] = 'menu_order title'; - $args['order'] = $order == 'DESC' ? 'DESC' : 'ASC'; + $args['order'] = ( 'DESC' === $order ) ? 'DESC' : 'ASC'; $args['meta_key'] = ''; switch ( $orderby ) { case 'rand' : $args['orderby'] = 'rand'; - break; + break; case 'date' : - $args['orderby'] = 'date'; - $args['order'] = $order == 'ASC' ? 'ASC' : 'DESC'; - break; + $args['orderby'] = 'date ID'; + $args['order'] = ( 'ASC' === $order ) ? 'ASC' : 'DESC'; + break; case 'price' : - $args['orderby'] = 'meta_value_num'; - $args['order'] = $order == 'DESC' ? 'DESC' : 'ASC'; - $args['meta_key'] = '_price'; - break; + if ( 'DESC' === $order ) { + add_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) ); + } else { + add_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) ); + } + break; case 'popularity' : $args['meta_key'] = 'total_sales'; // Sorting handled later though a hook add_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); - break; + break; case 'rating' : - // Sorting handled later though a hook - add_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); - break; + $args['meta_key'] = '_wc_average_rating'; + $args['orderby'] = array( + 'meta_value_num' => 'DESC', + 'ID' => 'ASC', + ); + break; case 'title' : - $args['orderby'] = 'title'; - $args['order'] = $order == 'DESC' ? 'DESC' : 'ASC'; - break; + $args['orderby'] = 'title'; + $args['order'] = ( 'DESC' === $order ) ? 'DESC' : 'ASC'; + break; } return apply_filters( 'woocommerce_get_catalog_ordering_args', $args ); } /** - * WP Core doens't let us change the sort direction for invidual orderby params - http://core.trac.wordpress.org/ticket/17065 + * Handle numeric price sorting. + * + * @access public + * @param array $args + * @return array + */ + public function order_by_price_asc_post_clauses( $args ) { + global $wpdb; + $args['join'] .= " INNER JOIN ( SELECT post_id, min( meta_value+0 ) price FROM $wpdb->postmeta WHERE meta_key='_price' GROUP BY post_id ) as price_query ON $wpdb->posts.ID = price_query.post_id "; + $args['orderby'] = " price_query.price ASC "; + return $args; + } + + /** + * Handle numeric price sorting. + * + * @access public + * @param array $args + * @return array + */ + public function order_by_price_desc_post_clauses( $args ) { + global $wpdb; + $args['join'] .= " INNER JOIN ( SELECT post_id, max( meta_value+0 ) price FROM $wpdb->postmeta WHERE meta_key='_price' GROUP BY post_id ) as price_query ON $wpdb->posts.ID = price_query.post_id "; + $args['orderby'] = " price_query.price DESC "; + return $args; + } + + /** + * WP Core doens't let us change the sort direction for invidual orderby params - https://core.trac.wordpress.org/ticket/17065. * * This lets us sort by meta value desc, and have a second orderby param. * @@ -563,33 +543,29 @@ class WC_Query { */ public function order_by_popularity_post_clauses( $args ) { global $wpdb; - $args['orderby'] = "$wpdb->postmeta.meta_value+0 DESC, $wpdb->posts.post_date DESC"; - return $args; } /** - * order_by_rating_post_clauses function. + * Order by rating post clauses. * - * @access public + * @deprecated 3.0.0 * @param array $args * @return array */ public function order_by_rating_post_clauses( $args ) { global $wpdb; + wc_deprecated_function( 'order_by_rating_post_clauses', '3.0' ); + $args['fields'] .= ", AVG( $wpdb->commentmeta.meta_value ) as average_rating "; - - $args['where'] .= " AND ( $wpdb->commentmeta.meta_key = 'rating' OR $wpdb->commentmeta.meta_key IS null ) "; - - $args['join'] .= " + $args['where'] .= " AND ( $wpdb->commentmeta.meta_key = 'rating' OR $wpdb->commentmeta.meta_key IS null ) "; + $args['join'] .= " LEFT OUTER JOIN $wpdb->comments ON($wpdb->posts.ID = $wpdb->comments.comment_post_ID) LEFT JOIN $wpdb->commentmeta ON($wpdb->comments.comment_ID = $wpdb->commentmeta.comment_id) "; - $args['orderby'] = "average_rating DESC, $wpdb->posts.post_date DESC"; - $args['groupby'] = "$wpdb->posts.ID"; return $args; @@ -597,256 +573,237 @@ class WC_Query { /** * Appends meta queries to an array. - * @access public - * @param array $meta_query + * + * @param array $meta_query + * @param bool $main_query * @return array */ - public function get_meta_query( $meta_query = array() ) { - if ( ! is_array( $meta_query ) ) + public function get_meta_query( $meta_query = array(), $main_query = false ) { + if ( ! is_array( $meta_query ) ) { $meta_query = array(); - - $meta_query[] = $this->visibility_meta_query(); - $meta_query[] = $this->stock_status_meta_query(); - - return array_filter( $meta_query ); + } + $meta_query['price_filter'] = $this->price_filter_meta_query(); + return array_filter( apply_filters( 'woocommerce_product_query_meta_query', $meta_query, $this ) ); } /** - * Returns a meta query to handle product visibility + * Appends tax queries to an array. + * @param array $tax_query + * @param bool $main_query + * @return array + */ + public function get_tax_query( $tax_query = array(), $main_query = false ) { + if ( ! is_array( $tax_query ) ) { + $tax_query = array( 'relation' => 'AND' ); + } + + // Layered nav filters on terms. + if ( $main_query && ( $_chosen_attributes = $this->get_layered_nav_chosen_attributes() ) ) { + foreach ( $_chosen_attributes as $taxonomy => $data ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'slug', + 'terms' => $data['terms'], + 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', + 'include_children' => false, + ); + } + } + + $product_visibility_terms = wc_get_product_visibility_term_ids(); + $product_visibility_not_in = array( is_search() && $main_query ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] ); + + // Hide out of stock products. + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $product_visibility_not_in[] = $product_visibility_terms['outofstock']; + } + + // Filter by rating. + if ( isset( $_GET['rating_filter'] ) ) { + $rating_filter = array_filter( array_map( 'absint', explode( ',', $_GET['rating_filter'] ) ) ); + $rating_terms = array(); + for ( $i = 1; $i <= 5; $i ++ ) { + if ( in_array( $i, $rating_filter ) && isset( $product_visibility_terms[ 'rated-' . $i ] ) ) { + $rating_terms[] = $product_visibility_terms[ 'rated-' . $i ]; + } + } + if ( ! empty( $rating_terms ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $rating_terms, + 'operator' => 'IN', + 'rating_filter' => true, + ); + } + } + + if ( ! empty( $product_visibility_not_in ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_not_in, + 'operator' => 'NOT IN', + ); + } + + return array_filter( apply_filters( 'woocommerce_product_query_tax_query', $tax_query, $this ) ); + } + + /** + * Return a meta query for filtering by price. + * @return array + */ + private function price_filter_meta_query() { + if ( isset( $_GET['max_price'] ) || isset( $_GET['min_price'] ) ) { + $meta_query = wc_get_min_max_price_meta_query( $_GET ); + $meta_query['price_filter'] = true; + + return $meta_query; + } + + return array(); + } + + /** + * Return a meta query for filtering by rating. * - * @access public + * @deprecated 3.0.0 Replaced with taxonomy. + * @return array + */ + public function rating_filter_meta_query() { + return array(); + } + + /** + * Returns a meta query to handle product visibility. + * + * @deprecated 3.0.0 Replaced with taxonomy. * @param string $compare (default: 'IN') * @return array */ public function visibility_meta_query( $compare = 'IN' ) { - if ( is_search() ) - $in = array( 'visible', 'search' ); - else - $in = array( 'visible', 'catalog' ); - - $meta_query = array( - 'key' => '_visibility', - 'value' => $in, - 'compare' => $compare - ); - - return $meta_query; + return array(); } /** - * Returns a meta query to handle product stock status + * Returns a meta query to handle product stock status. * - * @access public + * @deprecated 3.0.0 Replaced with taxonomy. * @param string $status (default: 'instock') * @return array */ public function stock_status_meta_query( $status = 'instock' ) { - $meta_query = array(); - if ( get_option( 'woocommerce_hide_out_of_stock_items' ) == 'yes' ) { - $meta_query = array( - 'key' => '_stock_status', - 'value' => $status, - 'compare' => '=' - ); - } + return array(); + } + + /** + * Get the tax query which was used by the main query. + * @return array + */ + public static function get_main_tax_query() { + global $wp_the_query; + + $tax_query = isset( $wp_the_query->tax_query, $wp_the_query->tax_query->queries ) ? $wp_the_query->tax_query->queries : array(); + + return $tax_query; + } + + /** + * Get the meta query which was used by the main query. + * @return array + */ + public static function get_main_meta_query() { + global $wp_the_query; + + $args = $wp_the_query->query_vars; + $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); + return $meta_query; } /** - * Layered Nav Init + * Based on WP_Query::parse_search */ - public function layered_nav_init( ) { + public static function get_main_search_query_sql() { + global $wp_the_query, $wpdb; - if ( is_active_widget( false, false, 'woocommerce_layered_nav', true ) && ! is_admin() ) { + $args = $wp_the_query->query_vars; + $search_terms = isset( $args['search_terms'] ) ? $args['search_terms'] : array(); + $sql = array(); - global $_chosen_attributes; + foreach ( $search_terms as $term ) { + // Terms prefixed with '-' should be excluded. + $include = '-' !== substr( $term, 0, 1 ); - $_chosen_attributes = array(); + if ( $include ) { + $like_op = 'LIKE'; + $andor_op = 'OR'; + } else { + $like_op = 'NOT LIKE'; + $andor_op = 'AND'; + $term = substr( $term, 1 ); + } - $attribute_taxonomies = wc_get_attribute_taxonomies(); - if ( $attribute_taxonomies ) { - foreach ( $attribute_taxonomies as $tax ) { + $like = '%' . $wpdb->esc_like( $term ) . '%'; + $sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like ); + } - $attribute = wc_sanitize_taxonomy_name( $tax->attribute_name ); - $taxonomy = wc_attribute_taxonomy_name( $attribute ); - $name = 'filter_' . $attribute; - $query_type_name = 'query_type_' . $attribute; + if ( ! empty( $sql ) && ! is_user_logged_in() ) { + $sql[] = "($wpdb->posts.post_password = '')"; + } - if ( ! empty( $_GET[ $name ] ) && taxonomy_exists( $taxonomy ) ) { - - $_chosen_attributes[ $taxonomy ]['terms'] = explode( ',', $_GET[ $name ] ); - - if ( empty( $_GET[ $query_type_name ] ) || ! in_array( strtolower( $_GET[ $query_type_name ] ), array( 'and', 'or' ) ) ) - $_chosen_attributes[ $taxonomy ]['query_type'] = apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' ); - else - $_chosen_attributes[ $taxonomy ]['query_type'] = strtolower( $_GET[ $query_type_name ] ); - - } - } - } - - add_filter('loop_shop_post_in', array( $this, 'layered_nav_query' ) ); - } + return implode( ' AND ', $sql ); } /** - * Layered Nav post filter + * Layered Nav Init. + */ + public static function get_layered_nav_chosen_attributes() { + if ( ! is_array( self::$_chosen_attributes ) ) { + self::$_chosen_attributes = array(); + + if ( $attribute_taxonomies = wc_get_attribute_taxonomies() ) { + foreach ( $attribute_taxonomies as $tax ) { + $attribute = wc_sanitize_taxonomy_name( $tax->attribute_name ); + $taxonomy = wc_attribute_taxonomy_name( $attribute ); + $filter_terms = ! empty( $_GET[ 'filter_' . $attribute ] ) ? explode( ',', wc_clean( $_GET[ 'filter_' . $attribute ] ) ) : array(); + + if ( empty( $filter_terms ) || ! taxonomy_exists( $taxonomy ) ) { + continue; + } + + $query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ) ) ? wc_clean( $_GET[ 'query_type_' . $attribute ] ) : ''; + self::$_chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding + self::$_chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' ); + } + } + } + return self::$_chosen_attributes; + } + + /** + * @deprecated 2.6.0 + */ + public function layered_nav_init() { + wc_deprecated_function( 'layered_nav_init', '2.6' ); + } + + /** + * Get an unpaginated list all product IDs (both filtered and unfiltered). Makes use of transients. + * @deprecated 2.6.0 due to performance concerns + */ + public function get_products_in_view() { + wc_deprecated_function( 'get_products_in_view', '2.6' ); + } + + /** + * Layered Nav post filter. + * @deprecated 2.6.0 due to performance concerns * - * @param array $filtered_posts - * @return array + * @param $filtered_posts */ public function layered_nav_query( $filtered_posts ) { - global $_chosen_attributes; - - if ( sizeof( $_chosen_attributes ) > 0 ) { - - $matched_products = array( - 'and' => array(), - 'or' => array() - ); - $filtered_attribute = array( - 'and' => false, - 'or' => false - ); - - foreach ( $_chosen_attributes as $attribute => $data ) { - $matched_products_from_attribute = array(); - $filtered = false; - - if ( sizeof( $data['terms'] ) > 0 ) { - foreach ( $data['terms'] as $value ) { - - $posts = get_posts( - array( - 'post_type' => 'product', - 'numberposts' => -1, - 'post_status' => 'publish', - 'fields' => 'ids', - 'no_found_rows' => true, - 'tax_query' => array( - array( - 'taxonomy' => $attribute, - 'terms' => $value, - 'field' => 'term_id' - ) - ) - ) - ); - - if ( ! is_wp_error( $posts ) ) { - - if ( sizeof( $matched_products_from_attribute ) > 0 || $filtered ) - $matched_products_from_attribute = $data['query_type'] == 'or' ? array_merge( $posts, $matched_products_from_attribute ) : array_intersect( $posts, $matched_products_from_attribute ); - else - $matched_products_from_attribute = $posts; - - $filtered = true; - } - } - } - - if ( sizeof( $matched_products[ $data['query_type'] ] ) > 0 || $filtered_attribute[ $data['query_type'] ] === true ) { - $matched_products[ $data['query_type'] ] = ( $data['query_type'] == 'or' ) ? array_merge( $matched_products_from_attribute, $matched_products[ $data['query_type'] ] ) : array_intersect( $matched_products_from_attribute, $matched_products[ $data['query_type'] ] ); - } else { - $matched_products[ $data['query_type'] ] = $matched_products_from_attribute; - } - - $filtered_attribute[ $data['query_type'] ] = true; - - $this->filtered_product_ids_for_taxonomy[ $attribute ] = $matched_products_from_attribute; - } - - // Combine our AND and OR result sets - if ( $filtered_attribute['and'] && $filtered_attribute['or'] ) - $results = array_intersect( $matched_products[ 'and' ], $matched_products[ 'or' ] ); - else - $results = array_merge( $matched_products[ 'and' ], $matched_products[ 'or' ] ); - - if ( $filtered ) { - - WC()->query->layered_nav_post__in = $results; - WC()->query->layered_nav_post__in[] = 0; - - if ( sizeof( $filtered_posts ) == 0 ) { - $filtered_posts = $results; - $filtered_posts[] = 0; - } else { - $filtered_posts = array_intersect( $filtered_posts, $results ); - $filtered_posts[] = 0; - } - - } - } - return (array) $filtered_posts; + wc_deprecated_function( 'layered_nav_query', '2.6' ); } - - /** - * Price filter Init - */ - public function price_filter_init() { - if ( is_active_widget( false, false, 'woocommerce_price_filter', true ) && ! is_admin() ) { - - $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; - - wp_register_script( 'wc-price-slider', WC()->plugin_url() . '/assets/js/frontend/price-slider' . $suffix . '.js', array( 'jquery-ui-slider' ), WC_VERSION, true ); - - wp_localize_script( 'wc-price-slider', 'woocommerce_price_slider_params', array( - 'currency_symbol' => get_woocommerce_currency_symbol(), - 'currency_pos' => get_option( 'woocommerce_currency_pos' ), - 'min_price' => isset( $_GET['min_price'] ) ? esc_attr( $_GET['min_price'] ) : '', - 'max_price' => isset( $_GET['max_price'] ) ? esc_attr( $_GET['max_price'] ) : '' - ) ); - - add_filter( 'loop_shop_post_in', array( $this, 'price_filter' ) ); - } - } - - /** - * Price Filter post filter - * - * @param array $filtered_posts - * @return array - */ - public function price_filter( $filtered_posts ) { - global $wpdb; - - if ( isset( $_GET['max_price'] ) && isset( $_GET['min_price'] ) ) { - - $matched_products = array(); - $min = floatval( $_GET['min_price'] ); - $max = floatval( $_GET['max_price'] ); - - $matched_products_query = apply_filters( 'woocommerce_price_filter_results', $wpdb->get_results( $wpdb->prepare(" - SELECT DISTINCT ID, post_parent, post_type FROM $wpdb->posts - INNER JOIN $wpdb->postmeta ON ID = post_id - WHERE post_type IN ( 'product', 'product_variation' ) AND post_status = 'publish' AND meta_key = %s AND meta_value BETWEEN %d AND %d - ", '_price', $min, $max ), OBJECT_K ), $min, $max ); - - if ( $matched_products_query ) { - foreach ( $matched_products_query as $product ) { - if ( $product->post_type == 'product' ) - $matched_products[] = $product->ID; - if ( $product->post_parent > 0 && ! in_array( $product->post_parent, $matched_products ) ) - $matched_products[] = $product->post_parent; - } - } - - // Filter the id's - if ( sizeof( $filtered_posts ) == 0) { - $filtered_posts = $matched_products; - $filtered_posts[] = 0; - } else { - $filtered_posts = array_intersect( $filtered_posts, $matched_products ); - $filtered_posts[] = 0; - } - - } - - return (array) $filtered_posts; - } - } - -endif; - -return new WC_Query(); diff --git a/includes/class-wc-register-wp-admin-settings.php b/includes/class-wc-register-wp-admin-settings.php new file mode 100644 index 00000000000..4f197f9db98 --- /dev/null +++ b/includes/class-wc-register-wp-admin-settings.php @@ -0,0 +1,174 @@ +object = $object; + + if ( 'page' === $type ) { + add_filter( 'woocommerce_settings_groups', array( $this, 'register_page_group' ) ); + add_filter( 'woocommerce_settings-' . $this->object->get_id(), array( $this, 'register_page_settings' ) ); + } elseif ( 'email' === $type ) { + add_filter( 'woocommerce_settings_groups', array( $this, 'register_email_group' ) ); + add_filter( 'woocommerce_settings-email_' . $this->object->id, array( $this, 'register_email_settings' ) ); + } + } + + /** + * Register's all of our different notification emails as sub groups + * of email settings. + * + * @since 3.0.0 + * @param array $groups Existing registered groups. + * @return array + */ + public function register_email_group( $groups ) { + $groups[] = array( + 'id' => 'email_' . $this->object->id, + 'label' => $this->object->title, + 'description' => $this->object->description, + 'parent_id' => 'email', + ); + return $groups; + } + + /** + * Registers all of the setting form fields for emails to each email type's group. + * + * @since 3.0.0 + * @param array $settings Existing registered settings. + * @return array + */ + public function register_email_settings( $settings ) { + foreach ( $this->object->form_fields as $id => $setting ) { + $setting['id'] = $id; + $setting['option_key'] = array( $this->object->get_option_key(), $id ); + $new_setting = $this->register_setting( $setting ); + if ( $new_setting ) { + $settings[] = $new_setting; + } + } + return $settings; + } + + /** + * Registers a setting group, based on admin page ID & label as parent group. + * + * @since 3.0.0 + * @param array $groups Array of previously registered groups. + * @return array + */ + public function register_page_group( $groups ) { + $groups[] = array( + 'id' => $this->object->get_id(), + 'label' => $this->object->get_label(), + ); + return $groups; + } + + /** + * Registers settings to a specific group. + * + * @since 3.0.0 + * @param array $settings Existing registered settings + * @return array + */ + public function register_page_settings( $settings ) { + /** + * wp-admin settings can be broken down into separate sections from + * a UI standpoint. This will grab all the sections associated with + * a particular setting group (like 'products') and register them + * to the REST API. + */ + $sections = $this->object->get_sections(); + if ( empty( $sections ) ) { + // Default section is just an empty string, per admin page classes + $sections = array( '' ); + } + + foreach ( $sections as $section => $section_label ) { + $settings_from_section = $this->object->get_settings( $section ); + foreach ( $settings_from_section as $setting ) { + if ( ! isset( $setting['id'] ) ) { + continue; + } + $setting['option_key'] = $setting['id']; + $new_setting = $this->register_setting( $setting ); + if ( $new_setting ) { + $settings[] = $new_setting; + } + } + } + return $settings; + } + + /** + * Register a setting into the format expected for the Settings REST API. + * + * @since 3.0.0 + * @param array $setting + * @return array|bool + */ + public function register_setting( $setting ) { + if ( ! isset( $setting['id'] ) ) { + return false; + } + + $description = ''; + if ( ! empty( $setting['desc'] ) ) { + $description = $setting['desc']; + } elseif ( ! empty( $setting['description'] ) ) { + $description = $setting['description']; + } + + $new_setting = array( + 'id' => $setting['id'], + 'label' => ( ! empty( $setting['title'] ) ? $setting['title'] : '' ), + 'description' => $description, + 'type' => $setting['type'], + 'option_key' => $setting['option_key'], + ); + + if ( isset( $setting['default'] ) ) { + $new_setting['default'] = $setting['default']; + } + if ( isset( $setting['options'] ) ) { + $new_setting['options'] = $setting['options']; + } + if ( isset( $setting['desc_tip'] ) ) { + if ( true === $setting['desc_tip'] ) { + $new_setting['tip'] = $description; + } elseif ( ! empty( $setting['desc_tip'] ) ) { + $new_setting['tip'] = $setting['desc_tip']; + } + } + + return $new_setting; + } + +} diff --git a/includes/class-wc-session-handler.php b/includes/class-wc-session-handler.php index 29fbe6a69c9..13bd8ba35d3 100644 --- a/includes/class-wc-session-handler.php +++ b/includes/class-wc-session-handler.php @@ -1,44 +1,45 @@ _cookie = 'wp_woocommerce_session_' . COOKIEHASH; + global $wpdb; + + $this->_cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ); + $this->_table = $wpdb->prefix . 'woocommerce_sessions'; if ( $cookie = $this->get_session_cookie() ) { $this->_customer_id = $cookie[0]; @@ -49,15 +50,8 @@ class WC_Session_Handler extends WC_Session { // Update session if its close to expiring if ( time() > $this->_session_expiring ) { $this->set_session_expiration(); - $session_expiry_option = '_wc_session_expires_' . $this->_customer_id; - // Check if option exists first to avoid auloading cleaned up sessions - if ( false === get_option( $session_expiry_option ) ) { - add_option( $session_expiry_option, $this->_session_expiration, '', 'no' ); - } else { - update_option( $session_expiry_option, $this->_session_expiration ); - } + $this->update_session_timestamp( $this->_customer_id, $this->_session_expiration ); } - } else { $this->set_session_expiration(); $this->_customer_id = $this->generate_customer_id(); @@ -65,91 +59,89 @@ class WC_Session_Handler extends WC_Session { $this->_data = $this->get_session_data(); - // Actions - add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 ); - add_action( 'woocommerce_cleanup_sessions', array( $this, 'cleanup_sessions' ), 10 ); - add_action( 'shutdown', array( $this, 'save_data' ), 20 ); - add_action( 'clear_auth_cookie', array( $this, 'destroy_session' ) ); - if ( ! is_user_logged_in() ) { - add_action( 'woocommerce_thankyou', array( $this, 'destroy_session' ) ); - } - } + // Actions + add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 ); + add_action( 'woocommerce_cleanup_sessions', array( $this, 'cleanup_sessions' ), 10 ); + add_action( 'shutdown', array( $this, 'save_data' ), 20 ); + add_action( 'wp_logout', array( $this, 'destroy_session' ) ); + if ( ! is_user_logged_in() ) { + add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ) ); + } + } - /** - * Sets the session cookie on-demand (usually after adding an item to the cart). - * - * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set. - * - * Warning: Cookies will only be set if this is called before the headers are sent. - */ - public function set_customer_session_cookie( $set ) { - if ( $set ) { - // Set/renew our cookie - $to_hash = $this->_customer_id . $this->_session_expiration; + /** + * Sets the session cookie on-demand (usually after adding an item to the cart). + * + * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set. + * + * Warning: Cookies will only be set if this is called before the headers are sent. + * + * @param bool $set + */ + public function set_customer_session_cookie( $set ) { + if ( $set ) { + // Set/renew our cookie + $to_hash = $this->_customer_id . '|' . $this->_session_expiration; $cookie_hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); $cookie_value = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash; $this->_has_cookie = true; - // Set the cookie - wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, apply_filters( 'wc_session_use_secure_cookie', false ) ); - } - } - - /** - * Return true if the current user has an active session, i.e. a cookie to retrieve values - * @return boolean - */ - public function has_session() { - return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); - } - - /** - * set_session_expiration function. - * - * @access public - * @return void - */ - public function set_session_expiration() { - $this->_session_expiring = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours - $this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours - } + // Set the cookie + wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, apply_filters( 'wc_session_use_secure_cookie', false ) ); + } + } /** - * Generate a unique customer ID for guests, or return user ID if logged in. - * + * Return true if the current user has an active session, i.e. a cookie to retrieve values. + * + * @return bool + */ + public function has_session() { + return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); + } + + /** + * Set session expiration. + */ + public function set_session_expiration() { + $this->_session_expiring = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours. + $this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours. + } + + /** + * Generate a unique customer ID for guests, or return user ID if logged in. + * * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID. * - * @access public * @return int|string */ public function generate_customer_id() { if ( is_user_logged_in() ) { return get_current_user_id(); } else { - require_once( ABSPATH . 'wp-includes/class-phpass.php'); + require_once( ABSPATH . 'wp-includes/class-phpass.php' ); $hasher = new PasswordHash( 8, false ); return md5( $hasher->get_random_bytes( 32 ) ); } } /** - * get_session_cookie function. + * Get session cookie. * - * @access public - * @return mixed + * @return bool|array */ public function get_session_cookie() { - if ( empty( $_COOKIE[ $this->_cookie ] ) ) { + if ( empty( $_COOKIE[ $this->_cookie ] ) || ! is_string( $_COOKIE[ $this->_cookie ] ) ) { return false; } list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $_COOKIE[ $this->_cookie ] ); // Validate hash - $to_hash = $customer_id . $session_expiration; + $to_hash = $customer_id . '|' . $session_expiration; $hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); - if ( $hash != $cookie_hash ) { + if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) { return false; } @@ -157,89 +149,166 @@ class WC_Session_Handler extends WC_Session { } /** - * get_session_data function. + * Get session data. * - * @access public * @return array */ public function get_session_data() { - return (array) get_option( '_wc_session_' . $this->_customer_id, array() ); + return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array(); } - /** - * save_data function. - * - * @access public - * @return void - */ - public function save_data() { - // Dirty if something changed - prevents saving nothing new - if ( $this->_dirty && $this->has_session() ) { + /** + * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call. + * + * @return string + */ + private function get_cache_prefix() { + return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ); + } - $session_option = '_wc_session_' . $this->_customer_id; - $session_expiry_option = '_wc_session_expires_' . $this->_customer_id; + /** + * Save data. + */ + public function save_data() { + // Dirty if something changed - prevents saving nothing new + if ( $this->_dirty && $this->has_session() ) { + global $wpdb; - if ( false === get_option( $session_option ) ) { - add_option( $session_option, $this->_data, '', 'no' ); - add_option( $session_expiry_option, $this->_session_expiration, '', 'no' ); - } else { - update_option( $session_option, $this->_data ); - } - } - } + $wpdb->replace( + $this->_table, + array( + 'session_key' => $this->_customer_id, + 'session_value' => maybe_serialize( $this->_data ), + 'session_expiry' => $this->_session_expiration, + ), + array( + '%s', + '%s', + '%d', + ) + ); - /** - * Destroy all session data - */ - public function destroy_session() { + // Set cache + wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() ); + + // Mark session clean after saving + $this->_dirty = false; + } + } + + /** + * Destroy all session data. + */ + public function destroy_session() { // Clear cookie wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, apply_filters( 'wc_session_use_secure_cookie', false ) ); - // Delete session - $session_option = '_wc_session_' . $this->_customer_id; - $session_expiry_option = '_wc_session_expires_' . $this->_customer_id; - - delete_option( $session_option ); - delete_option( $session_expiry_option ); + $this->delete_session( $this->_customer_id ); // Clear cart wc_empty_cart(); - + // Clear data $this->_data = array(); $this->_dirty = false; $this->_customer_id = $this->generate_customer_id(); } - /** - * cleanup_sessions function. + /** + * When a user is logged out, ensure they have a unique nonce by using the customer/session ID. * - * @access public - * @return void + * @param int $uid + * + * @return string + */ + public function nonce_user_logged_out( $uid ) { + return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid; + } + + /** + * Cleanup sessions. */ public function cleanup_sessions() { global $wpdb; if ( ! defined( 'WP_SETUP_CONFIG' ) && ! defined( 'WP_INSTALLING' ) ) { - $now = time(); - $expired_sessions = array(); - $wc_session_expires = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE '_wc_session_expires_%'" ); - foreach ( $wc_session_expires as $wc_session_expire ) { - if ( $now > intval( $wc_session_expire->option_value ) ) { - $session_id = substr( $wc_session_expire->option_name, 20 ); - $expired_sessions[] = $wc_session_expire->option_name; // Expires key - $expired_sessions[] = "_wc_session_$session_id"; // Session key - } - } + // Delete expired sessions + $wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) ); - if ( ! empty( $expired_sessions ) ) { - $expired_sessions_chunked = array_chunk( $expired_sessions, 100 ); - foreach ( $expired_sessions_chunked as $chunk ) { - $option_names = implode( "','", $chunk ); - $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name IN ('$option_names')" ); - } - } + // Invalidate cache + WC_Cache_Helper::incr_cache_prefix( WC_SESSION_CACHE_GROUP ); } } -} \ No newline at end of file + + /** + * Returns the session. + * + * @param string $customer_id + * @param mixed $default + * @return string|array + */ + public function get_session( $customer_id, $default = false ) { + global $wpdb; + + if ( defined( 'WP_SETUP_CONFIG' ) ) { + return false; + } + + // Try get it from the cache, it will return false if not present or if object cache not in use + $value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); + + if ( false === $value ) { + $value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) ); + + if ( is_null( $value ) ) { + $value = $default; + } + + wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() ); + } + + return maybe_unserialize( $value ); + } + + /** + * Delete the session from the cache and database. + * + * @param int $customer_id + */ + public function delete_session( $customer_id ) { + global $wpdb; + + wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); + + $wpdb->delete( + $this->_table, + array( + 'session_key' => $customer_id, + ) + ); + } + + /** + * Update the session expiry timestamp. + * + * @param string $customer_id + * @param int $timestamp + */ + public function update_session_timestamp( $customer_id, $timestamp ) { + global $wpdb; + + $wpdb->update( + $this->_table, + array( + 'session_expiry' => $timestamp, + ), + array( + 'session_key' => $customer_id, + ), + array( + '%d' + ) + ); + } +} diff --git a/includes/class-wc-shipping-rate.php b/includes/class-wc-shipping-rate.php index 771083aba49..c7037928594 100644 --- a/includes/class-wc-shipping-rate.php +++ b/includes/class-wc-shipping-rate.php @@ -1,57 +1,94 @@ id = $id; - $this->label = $label; - $this->cost = $cost; - $this->taxes = $taxes ? $taxes : array(); - $this->method_id = $method_id; + private $meta_data = array(); + + /** + * Constructor. + * + * @param string $id + * @param string $label + * @param integer $cost + * @param array $taxes + * @param string $method_id + */ + public function __construct( $id = '', $label = '', $cost = 0, $taxes = array(), $method_id = '' ) { + $this->id = $id; + $this->label = $label; + $this->cost = $cost; + $this->taxes = ! empty( $taxes ) && is_array( $taxes ) ? $taxes : array(); + $this->method_id = $method_id; } /** - * get_shipping_tax function. + * Get shipping tax. * - * @access public * @return array */ - function get_shipping_tax() { - $taxes = 0; - if ( $this->taxes && sizeof( $this->taxes ) > 0 && ! WC()->customer->is_vat_exempt() ) { - $taxes = array_sum( $this->taxes ); - } - return $taxes; + public function get_shipping_tax() { + return apply_filters( 'woocommerce_get_shipping_tax', sizeof( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? array_sum( $this->taxes ) : 0, $this ); } -} \ No newline at end of file + + /** + * Get label. + * + * @return string + */ + public function get_label() { + return apply_filters( 'woocommerce_shipping_rate_label', $this->label, $this ); + } + + /** + * Add some meta data for this rate. + * @since 2.6.0 + * @param string $key + * @param string $value + */ + public function add_meta_data( $key, $value ) { + $this->meta_data[ wc_clean( $key ) ] = wc_clean( $value ); + } + + /** + * Get all meta data for this rate. + * @since 2.6.0 + */ + public function get_meta_data() { + return $this->meta_data; + } +} diff --git a/includes/class-wc-shipping-zone.php b/includes/class-wc-shipping-zone.php new file mode 100644 index 00000000000..fe171cf98a5 --- /dev/null +++ b/includes/class-wc-shipping-zone.php @@ -0,0 +1,435 @@ + '', + 'zone_order' => 0, + 'zone_locations' => array(), + ); + + /** + * Constructor for zones. + * + * @param int|object $zone Zone ID to load from the DB or zone object. + */ + public function __construct( $zone = null ) { + if ( is_numeric( $zone ) && ! empty( $zone ) ) { + $this->set_id( $zone ); + } elseif ( is_object( $zone ) ) { + $this->set_id( $zone->zone_id ); + } elseif ( 0 === $zone || "0" === $zone ) { + $this->set_id( 0 ); + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( 'shipping-zone' ); + if ( false === $this->get_object_read() ) { + $this->data_store->read( $this ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get zone name. + * + * @param string $context + * @return string + */ + public function get_zone_name( $context = 'view' ) { + return $this->get_prop( 'zone_name', $context ); + } + + /** + * Get zone order. + * + * @param string $context + * @return int + */ + public function get_zone_order( $context = 'view' ) { + return $this->get_prop( 'zone_order', $context ); + } + + /** + * Get zone locations. + * + * @param string $context + * @return array of zone objects + */ + public function get_zone_locations( $context = 'view' ) { + return $this->get_prop( 'zone_locations', $context ); + } + + /** + * Return a text string representing what this zone is for. + * + * @param int $max + * @param string $context + * @return string + */ + public function get_formatted_location( $max = 10, $context = 'view' ) { + $location_parts = array(); + $all_continents = WC()->countries->get_continents(); + $all_countries = WC()->countries->get_countries(); + $all_states = WC()->countries->get_states(); + $locations = $this->get_zone_locations( $context ); + $continents = array_filter( $locations, array( $this, 'location_is_continent' ) ); + $countries = array_filter( $locations, array( $this, 'location_is_country' ) ); + $states = array_filter( $locations, array( $this, 'location_is_state' ) ); + $postcodes = array_filter( $locations, array( $this, 'location_is_postcode' ) ); + + foreach ( $continents as $location ) { + $location_parts[] = $all_continents[ $location->code ]['name']; + } + + foreach ( $countries as $location ) { + $location_parts[] = $all_countries[ $location->code ]; + } + + foreach ( $states as $location ) { + $location_codes = explode( ':', $location->code ); + $location_parts[] = $all_states[ $location_codes[0] ][ $location_codes[1] ]; + } + + foreach ( $postcodes as $location ) { + $location_parts[] = $location->code; + } + + // Fix display of encoded characters. + $location_parts = array_map( 'html_entity_decode', $location_parts ); + + if ( sizeof( $location_parts ) > $max ) { + $remaining = sizeof( $location_parts ) - $max; + // @codingStandardsIgnoreStart + return sprintf( _n( '%s and %d other region', '%s and %d other regions', $remaining, 'woocommerce' ), implode( ', ', array_splice( $location_parts, 0, $max ) ), $remaining ); + // @codingStandardsIgnoreEnd + } elseif ( ! empty( $location_parts ) ) { + return implode( ', ', $location_parts ); + } else { + return __( 'Everywhere', 'woocommerce' ); + } + } + + /** + * Get shipping methods linked to this zone. + * + * @param bool Only return enabled methods. + * @return array of objects + */ + public function get_shipping_methods( $enabled_only = false ) { + if ( null === $this->get_id() ) { + return array(); + } + + $raw_methods = $this->data_store->get_methods( $this->get_id(), $enabled_only ); + $wc_shipping = WC_Shipping::instance(); + $allowed_classes = $wc_shipping->get_shipping_method_class_names(); + $methods = array(); + + foreach ( $raw_methods as $raw_method ) { + if ( in_array( $raw_method->method_id, array_keys( $allowed_classes ), true ) ) { + $class_name = $allowed_classes[ $raw_method->method_id ]; + + // The returned array may contain instances of shipping methods, as well + // as classes. If the "class" is an instance, just use it. If not, + // create an instance. + if ( is_object( $class_name ) ) { + $class_name_of_instance = get_class( $class_name ); + $methods[ $raw_method->instance_id ] = new $class_name_of_instance( $raw_method->instance_id ); + } else { + // If the class is not an object, it should be a string. It's better + // to double check, to be sure (a class must be a string, anything) + // else would be useless + if ( is_string( $class_name ) && class_exists( $class_name ) ) { + $methods[ $raw_method->instance_id ] = new $class_name( $raw_method->instance_id ); + } + } + + // Let's make sure that we have an instance before setting its attributes + if ( is_object( $methods[ $raw_method->instance_id ] ) ) { + $methods[ $raw_method->instance_id ]->method_order = absint( $raw_method->method_order ); + $methods[ $raw_method->instance_id ]->enabled = $raw_method->is_enabled ? 'yes' : 'no'; + $methods[ $raw_method->instance_id ]->has_settings = $methods[ $raw_method->instance_id ]->has_settings(); + $methods[ $raw_method->instance_id ]->settings_html = $methods[ $raw_method->instance_id ]->supports( 'instance-settings-modal' ) ? $methods[ $raw_method->instance_id ]->get_admin_options_html() : false; + $methods[ $raw_method->instance_id ]->method_description = wp_kses_post( wpautop( $methods[ $raw_method->instance_id ]->method_description ) ); + } + } + } + + uasort( $methods, 'wc_shipping_zone_method_order_uasort_comparison' ); + + return apply_filters( 'woocommerce_shipping_zone_shipping_methods', $methods, $raw_methods, $allowed_classes, $this ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set zone name. + * + * @param string $set + */ + public function set_zone_name( $set ) { + $this->set_prop( 'zone_name', wc_clean( $set ) ); + } + + /** + * Set zone order. + * + * @param int $set + */ + public function set_zone_order( $set ) { + $this->set_prop( 'zone_order', absint( $set ) ); + } + + /** + * Set zone locations. + * + * @since 3.0.0 + * @param array + */ + public function set_zone_locations( $locations ) { + if ( 0 !== $this->get_id() ) { + $this->set_prop( 'zone_locations', $locations ); + } + } + + /* + |-------------------------------------------------------------------------- + | Other Methods + |-------------------------------------------------------------------------- + */ + + /** + * Save zone data to the database. + * + * @return int + */ + public function save() { + if ( ! $this->get_zone_name() ) { + $this->set_zone_name( $this->generate_zone_name() ); + } + if ( $this->data_store ) { + // Trigger action before saving to the DB. Allows you to adjust object props before save. + do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); + + if ( null === $this->get_id() ) { + $this->data_store->create( $this ); + } else { + $this->data_store->update( $this ); + } + return $this->get_id(); + } + } + + /** + * Generate a zone name based on location. + * + * @return string + */ + protected function generate_zone_name() { + $zone_name = $this->get_formatted_location(); + + if ( empty( $zone_name ) ) { + $zone_name = __( 'Zone', 'woocommerce' ); + } + + return $zone_name; + } + + /** + * Location type detection. + * + * @param object $location + * @return boolean + */ + private function location_is_continent( $location ) { + return 'continent' === $location->type; + } + + /** + * Location type detection. + * + * @param object $location + * @return boolean + */ + private function location_is_country( $location ) { + return 'country' === $location->type; + } + + /** + * Location type detection. + * + * @param object $location + * @return boolean + */ + private function location_is_state( $location ) { + return 'state' === $location->type; + } + + /** + * Location type detection. + * + * @param object $location + * @return boolean + */ + private function location_is_postcode( $location ) { + return 'postcode' === $location->type; + } + + /** + * Is passed location type valid? + * + * @param string $type + * @return boolean + */ + public function is_valid_location_type( $type ) { + return in_array( $type, array( 'postcode', 'state', 'country', 'continent' ) ); + } + + /** + * Add location (state or postcode) to a zone. + * + * @param string $code + * @param string $type state or postcode + */ + public function add_location( $code, $type ) { + if ( 0 !== $this->get_id() && $this->is_valid_location_type( $type ) ) { + if ( 'postcode' === $type ) { + $code = trim( strtoupper( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $code ) ) ); // No normalization - postcodes are matched against both normal and formatted versions to support wildcards. + } + $location = array( + 'code' => wc_clean( $code ), + 'type' => wc_clean( $type ), + ); + $zone_locations = $this->get_prop( 'zone_locations', 'edit' ); + $zone_locations[] = (object) $location; + $this->set_prop( 'zone_locations', $zone_locations ); + } + } + + + /** + * Clear all locations for this zone. + * + * @param array|string $types of location to clear + */ + public function clear_locations( $types = array( 'postcode', 'state', 'country', 'continent' ) ) { + if ( ! is_array( $types ) ) { + $types = array( $types ); + } + $zone_locations = $this->get_prop( 'zone_locations', 'edit' ); + foreach ( $zone_locations as $key => $values ) { + if ( in_array( $values->type, $types ) ) { + unset( $zone_locations[ $key ] ); + } + } + $zone_locations = array_values( $zone_locations ); // reindex. + $this->set_prop( 'zone_locations', $zone_locations ); + } + + /** + * Set locations. + * + * @param array $locations Array of locations + */ + public function set_locations( $locations = array() ) { + $this->clear_locations(); + foreach ( $locations as $location ) { + $this->add_location( $location['code'], $location['type'] ); + } + } + + /** + * Add a shipping method to this zone. + * + * @param string $type shipping method type + * @return int new instance_id, 0 on failure + */ + public function add_shipping_method( $type ) { + if ( null === $this->get_id() ) { + $this->save(); + } + + $instance_id = 0; + $wc_shipping = WC_Shipping::instance(); + $allowed_classes = $wc_shipping->get_shipping_method_class_names(); + $count = $this->data_store->get_method_count( $this->get_id() ); + + if ( in_array( $type, array_keys( $allowed_classes ) ) ) { + $instance_id = $this->data_store->add_method( $this->get_id(), $type, $count + 1 ); + } + + if ( $instance_id ) { + do_action( 'woocommerce_shipping_zone_method_added', $instance_id, $type, $this->get_id() ); + } + + WC_Cache_Helper::get_transient_version( 'shipping', true ); + + return $instance_id; + } + + /** + * Delete a shipping method from a zone. + * + * @param int $instance_id + * @return True on success, false on failure + */ + public function delete_shipping_method( $instance_id ) { + if ( null === $this->get_id() ) { + return false; + } + + // Get method details. + $method = $this->data_store->get_method( $instance_id ); + + if ( $method ) { + $this->data_store->delete_method( $instance_id ); + do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method->method_id, $this->get_id() ); + } + + WC_Cache_Helper::get_transient_version( 'shipping', true ); + + return true; + } +} diff --git a/includes/class-wc-shipping-zones.php b/includes/class-wc-shipping-zones.php new file mode 100644 index 00000000000..2fe22367b27 --- /dev/null +++ b/includes/class-wc-shipping-zones.php @@ -0,0 +1,135 @@ +get_zones(); + $zones = array(); + + foreach ( $raw_zones as $raw_zone ) { + $zone = new WC_Shipping_Zone( $raw_zone ); + $zones[ $zone->get_id() ] = $zone->get_data(); + $zones[ $zone->get_id() ]['zone_id'] = $zone->get_id(); + $zones[ $zone->get_id() ]['formatted_zone_location'] = $zone->get_formatted_location(); + $zones[ $zone->get_id() ]['shipping_methods'] = $zone->get_shipping_methods(); + } + + return $zones; + } + + /** + * Get shipping zone using it's ID + * @since 2.6.0 + * @param int $zone_id + * @return WC_Shipping_Zone|bool + */ + public static function get_zone( $zone_id ) { + return self::get_zone_by( 'zone_id', $zone_id ); + } + + /** + * Get shipping zone by an ID. + * @since 2.6.0 + * @param string $by zone_id or instance_id + * @param int $id + * @return WC_Shipping_Zone|bool + */ + public static function get_zone_by( $by = 'zone_id', $id = 0 ) { + switch ( $by ) { + case 'zone_id' : + $zone_id = $id; + break; + case 'instance_id' : + $data_store = WC_Data_Store::load( 'shipping-zone' ); + $zone_id = $data_store->get_zone_id_by_instance_id( $id ); + break; + } + + if ( false !== $zone_id ) { + try { + return new WC_Shipping_Zone( $zone_id ); + } catch ( Exception $e ) { + return false; + } + } + + return false; + } + + /** + * Get shipping zone using it's ID + * @since 2.6.0 + * + * @param $instance_id + * + * @return bool|WC_Shipping_Meethod + */ + public static function get_shipping_method( $instance_id ) { + $data_store = WC_Data_Store::load( 'shipping-zone' ); + $raw_shipping_method = $data_store->get_method( $instance_id ); + $wc_shipping = WC_Shipping::instance(); + $allowed_classes = $wc_shipping->get_shipping_method_class_names(); + + if ( ! empty( $raw_shipping_method ) && in_array( $raw_shipping_method->method_id, array_keys( $allowed_classes ) ) ) { + $class_name = $allowed_classes[ $raw_shipping_method->method_id ]; + if ( is_object( $class_name ) ) { + $class_name = get_class( $class_name ); + } + return new $class_name( $raw_shipping_method->instance_id ); + } + return false; + } + + /** + * Delete a zone using it's ID + * @param int $zone_id + * @since 2.6.0 + */ + public static function delete_zone( $zone_id ) { + $zone = new WC_Shipping_Zone( $zone_id ); + $zone->delete(); + } + + /** + * Find a matching zone for a given package. + * @since 2.6.0 + * @uses wc_make_numeric_postcode() + * @param object $package + * @return WC_Shipping_Zone + */ + public static function get_zone_matching_package( $package ) { + $country = strtoupper( wc_clean( $package['destination']['country'] ) ); + $state = strtoupper( wc_clean( $package['destination']['state'] ) ); + $postcode = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) ); + $cache_key = WC_Cache_Helper::get_cache_prefix( 'shipping_zones' ) . 'wc_shipping_zone_' . md5( sprintf( '%s+%s+%s', $country, $state, $postcode ) ); + $matching_zone_id = wp_cache_get( $cache_key, 'shipping_zones' ); + + if ( false === $matching_zone_id ) { + $data_store = WC_Data_Store::load( 'shipping-zone' ); + $matching_zone_id = $data_store->get_zone_id_from_package( $package ); + wp_cache_set( $cache_key, $matching_zone_id, 'shipping_zones' ); + } + + return new WC_Shipping_Zone( $matching_zone_id ? $matching_zone_id : 0 ); + } +} diff --git a/includes/class-wc-shipping.php b/includes/class-wc-shipping.php index 09ded58b2f1..0a1e931f8a2 100644 --- a/includes/class-wc-shipping.php +++ b/includes/class-wc-shipping.php @@ -5,33 +5,38 @@ * Handles shipping and loads shipping methods via hooks. * * @class WC_Shipping - * @version 1.6.4 + * @version 2.6.0 * @package WooCommerce/Classes/Shipping * @category Class * @author WooThemes */ -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly +if ( ! defined( 'ABSPATH' ) ) { + exit; +} +/** + * WC_Shipping + */ class WC_Shipping { /** @var bool True if shipping is enabled. */ - var $enabled = false; + public $enabled = false; - /** @var array Stores methods loaded into woocommerce. */ - var $shipping_methods = array(); + /** @var array|null Stores methods loaded into woocommerce. */ + public $shipping_methods = null; /** @var float Stores the cost of shipping */ - var $shipping_total = 0; + public $shipping_total = 0; - /** @var array Stores an array of shipping taxes. */ - var $shipping_taxes = array(); + /** @var array Stores an array of shipping taxes. */ + public $shipping_taxes = array(); /** @var array Stores the shipping classes. */ - var $shipping_classes = array(); + public $shipping_classes = array(); /** @var array Stores packages to ship and to get quotes for. */ - var $packages = array(); + public $packages = array(); /** * @var WC_Shipping The single instance of the class @@ -40,7 +45,7 @@ class WC_Shipping { protected static $_instance = null; /** - * Main WC_Shipping Instance + * Main WC_Shipping Instance. * * Ensures only one instance of WC_Shipping is loaded or can be loaded. * @@ -49,8 +54,9 @@ class WC_Shipping { * @return WC_Shipping Main instance */ public static function instance() { - if ( is_null( self::$_instance ) ) + if ( is_null( self::$_instance ) ) { self::$_instance = new self(); + } return self::$_instance; } @@ -60,7 +66,7 @@ class WC_Shipping { * @since 2.1 */ public function __clone() { - _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); + wc_doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); } /** @@ -69,182 +75,200 @@ class WC_Shipping { * @since 2.1 */ public function __wakeup() { - _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); + wc_doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); } /** - * __construct function. - * - * @access public - * @return void + * Initialize shipping. */ public function __construct() { - $this->init(); - } + $this->enabled = wc_shipping_enabled(); - /** - * init function. - * - * @access public - */ - public function init() { - do_action( 'woocommerce_shipping_init' ); - - $this->enabled = ( get_option('woocommerce_calc_shipping') == 'no' ) ? false : true; + if ( $this->enabled ) { + $this->init(); + } + } + + /** + * Initialize shipping. + */ + public function init() { + do_action( 'woocommerce_shipping_init' ); + } + + /** + * Shipping methods register themselves by returning their main class name through the woocommerce_shipping_methods filter. + * @return array + */ + public function get_shipping_method_class_names() { + // Unique Method ID => Method Class name + $shipping_methods = array( + 'flat_rate' => 'WC_Shipping_Flat_Rate', + 'free_shipping' => 'WC_Shipping_Free_Shipping', + 'local_pickup' => 'WC_Shipping_Local_Pickup', + ); + + // For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here + $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); + + foreach ( $maybe_load_legacy_methods as $method ) { + $options = get_option( 'woocommerce_' . $method . '_settings' ); + if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { + $shipping_methods[ 'legacy_' . $method ] = 'WC_Shipping_Legacy_' . $method; + } + } + + return apply_filters( 'woocommerce_shipping_methods', $shipping_methods ); } /** - * load_shipping_methods function. - * * Loads all shipping methods which are hooked in. If a $package is passed some methods may add themselves conditionally. * - * Methods are sorted into their user-defined order after being loaded. + * Loads all shipping methods which are hooked in. + * If a $package is passed some methods may add themselves conditionally and zones will be used. * - * @access public * @param array $package * @return array */ public function load_shipping_methods( $package = array() ) { + if ( ! empty( $package ) ) { + $debug_mode = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ); + $shipping_zone = WC_Shipping_Zones::get_zone_matching_package( $package ); + $this->shipping_methods = $shipping_zone->get_shipping_methods( true ); - $this->unregister_shipping_methods(); - - // Methods can register themselves through this hook - do_action( 'woocommerce_load_shipping_methods', $package ); - - // Register methods through a filter - $shipping_methods_to_load = apply_filters( 'woocommerce_shipping_methods', array( - 'WC_Shipping_Flat_Rate', - 'WC_Shipping_Free_Shipping', - 'WC_Shipping_International_Delivery', - 'WC_Shipping_Local_Delivery', - 'WC_Shipping_Local_Pickup' - ) ); - - foreach ( $shipping_methods_to_load as $method ) - $this->register_shipping_method( $method ); - - $this->sort_shipping_methods(); - - return $this->shipping_methods; - } - - /** - * Register a shipping method for use in calculations. - * - * @access public - * @param object|string $method Either the name of the method's class, or an instance of the method's class - * @return void - */ - public function register_shipping_method( $method ) { - - if ( ! is_object( $method ) ) - $method = new $method(); - - $id = empty( $method->instance_id ) ? $method->id : $method->instance_id; - - $this->shipping_methods[ $id ] = $method; - } - - /** - * unregister_shipping_methods function. - * - * @access public - * @return void - */ - public function unregister_shipping_methods() { - unset( $this->shipping_methods ); - } - - /** - * sort_shipping_methods function. - * - * Sorts shipping methods into the user defined order. - * - * @access public - * @return array - */ - public function sort_shipping_methods() { - - $sorted_shipping_methods = array(); - - // Get order option - $ordering = (array) get_option('woocommerce_shipping_method_order'); - $order_end = 999; - - // Load shipping methods in order - foreach ( $this->shipping_methods as $method ) { - - if ( isset( $ordering[ $method->id ] ) && is_numeric( $ordering[ $method->id ] ) ) { - // Add in position - $sorted_shipping_methods[ $ordering[ $method->id ] ][] = $method; - } else { - // Add to end of the array - $sorted_shipping_methods[ $order_end ][] = $method; + // Debug output + if ( $debug_mode && ! defined( 'WOOCOMMERCE_CHECKOUT' ) && ! wc_has_notice( 'Customer matched zone "' . $shipping_zone->get_zone_name() . '"' ) ) { + wc_add_notice( 'Customer matched zone "' . $shipping_zone->get_zone_name() . '"' ); } + } else { + $this->shipping_methods = array(); } - ksort( $sorted_shipping_methods ); + // For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here. + foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) { + $this->register_shipping_method( $method_class ); + } - $this->shipping_methods = array(); + // Methods can register themselves manually through this hook if necessary. + do_action( 'woocommerce_load_shipping_methods', $package ); - foreach ( $sorted_shipping_methods as $methods ) - foreach ( $methods as $method ) { - $id = empty( $method->instance_id ) ? $method->id : $method->instance_id; - $this->shipping_methods[ $id ] = $method; - } - - return $this->shipping_methods; + // Return loaded methods + return $this->get_shipping_methods(); } /** - * get_shipping_methods function. + * Register a shipping method. * + * @param object|string $method Either the name of the method's class, or an instance of the method's class. + * + * @return bool|void + */ + public function register_shipping_method( $method ) { + if ( ! is_object( $method ) ) { + if ( ! class_exists( $method ) ) { + return false; + } + $method = new $method(); + } + if ( is_null( $this->shipping_methods ) ) { + $this->shipping_methods = array(); + } + $this->shipping_methods[ $method->id ] = $method; + } + + /** + * Unregister shipping methods. + */ + public function unregister_shipping_methods() { + $this->shipping_methods = null; + } + + /** * Returns all registered shipping methods for usage. * * @access public * @return array */ public function get_shipping_methods() { + if ( is_null( $this->shipping_methods ) ) { + $this->load_shipping_methods(); + } return $this->shipping_methods; } /** - * get_shipping_classes function. - * - * Load shipping classes taxonomy terms. + * Get an array of shipping classes. * * @access public * @return array */ public function get_shipping_classes() { - if ( empty( $this->shipping_classes ) ) - $this->shipping_classes = ( $classes = get_terms( 'product_shipping_class', array( 'hide_empty' => '0' ) ) ) ? $classes : array(); - - return $this->shipping_classes; + if ( empty( $this->shipping_classes ) ) { + $classes = get_terms( 'product_shipping_class', array( 'hide_empty' => '0', 'orderby' => 'name' ) ); + $this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array(); + } + return apply_filters( 'woocommerce_get_shipping_classes', $this->shipping_classes ); + } + + /** + * Get the default method. + * @param array $available_methods + * @param boolean $current_chosen_method + * @return string + */ + private function get_default_method( $available_methods, $current_chosen_method = false ) { + if ( ! empty( $available_methods ) ) { + if ( ! empty( $current_chosen_method ) ) { + if ( isset( $available_methods[ $current_chosen_method ] ) ) { + return $available_methods[ $current_chosen_method ]->id; + } else { + foreach ( $available_methods as $method_key => $method ) { + if ( strpos( $method->id, $current_chosen_method ) === 0 ) { + return $method->id; + } + } + } + } + return current( $available_methods )->id; + } + return ''; } /** - * calculate_shipping function. - * * Calculate shipping for (multiple) packages of cart items. * - * @access public * @param array $packages multi-dimensional array of cart items to calc shipping for */ public function calculate_shipping( $packages = array() ) { - if ( ! $this->enabled || empty( $packages ) ) - return; + $this->shipping_total = 0; + $this->shipping_taxes = array(); + $this->packages = array(); - $this->shipping_total = null; - $this->shipping_taxes = array(); - $this->packages = array(); + if ( ! $this->enabled || empty( $packages ) ) { + return; + } // Calculate costs for passed packages - $package_keys = array_keys( $packages ); - $package_keys_size = sizeof( $package_keys ); + foreach ( $packages as $package_key => $package ) { + $this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key ); + } - for ( $i = 0; $i < $package_keys_size; $i ++ ) - $this->packages[ $package_keys[ $i ] ] = $this->calculate_shipping_for_package( $packages[ $package_keys[ $i ] ] ); + /** + * Allow packages to be reorganized after calculate the shipping. + * + * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages + * but before Woocommerce does anything with them. A good example of usage is to merge the shipping methods for multiple + * packages for marketplaces. + * + * @since 2.6.0 + * + * @param array $packages The array of packages after shipping costs are calculated. + */ + $this->packages = apply_filters( 'woocommerce_shipping_packages', $this->packages ); + + if ( ! is_array( $this->packages ) || empty( $this->packages ) ) { + return; + } // Get all chosen methods $chosen_methods = WC()->session->get( 'chosen_shipping_methods' ); @@ -252,66 +276,38 @@ class WC_Shipping { // Get chosen methods for each package foreach ( $this->packages as $i => $package ) { - - $_cheapest_cost = false; - $_cheapest_method = false; $chosen_method = false; $method_count = false; - if ( ! empty( $chosen_methods[ $i ] ) ) + if ( ! empty( $chosen_methods[ $i ] ) ) { $chosen_method = $chosen_methods[ $i ]; + } - if ( ! empty( $method_counts[ $i ] ) ) - $method_count = $method_counts[ $i ]; + if ( ! empty( $method_counts[ $i ] ) ) { + $method_count = absint( $method_counts[ $i ] ); + } - // Get available methods for package - $_available_methods = $package['rates']; + if ( sizeof( $package['rates'] ) > 0 ) { - if ( sizeof( $_available_methods ) > 0 ) { - - // If not set, not available, or available methods have changed, set to the default option - if ( empty( $chosen_method ) || ! isset( $_available_methods[ $chosen_method ] ) || $method_count != sizeof( $_available_methods ) ) { - - $chosen_method = apply_filters( 'woocommerce_shipping_chosen_method', get_option( 'woocommerce_default_shipping_method' ), $_available_methods ); - - // Loops methods and find a match - if ( ! empty( $chosen_method ) && ! isset( $_available_methods[ $chosen_method ] ) ) { - foreach ( $_available_methods as $method_id => $method ) { - if ( strpos( $method->id, $chosen_method ) === 0 ) { - $chosen_method = $method->id; - break; - } - } - } - - if ( empty( $chosen_method ) || ! isset( $_available_methods[ $chosen_method ] ) ) { - // Default to cheapest - foreach ( $_available_methods as $method_id => $method ) { - if ( $method->cost < $_cheapest_cost || ! is_numeric( $_cheapest_cost ) ) { - $_cheapest_cost = $method->cost; - $_cheapest_method = $method_id; - } - } - $chosen_method = $_cheapest_method; - } - - // Store chosen method + // If not set, not available, or available methods have changed, set to the DEFAULT option + if ( empty( $chosen_method ) || ! isset( $package['rates'][ $chosen_method ] ) || sizeof( $package['rates'] ) !== $method_count ) { + $chosen_method = apply_filters( 'woocommerce_shipping_chosen_method', $this->get_default_method( $package['rates'], false ), $package['rates'], $chosen_method ); $chosen_methods[ $i ] = $chosen_method; - $method_counts[ $i ] = sizeof( $_available_methods ); - - // Do action for this chosen method + $method_counts[ $i ] = sizeof( $package['rates'] ); do_action( 'woocommerce_shipping_method_chosen', $chosen_method ); } // Store total costs - if ( $chosen_method ) { - $rate = $_available_methods[ $chosen_method ]; + if ( $chosen_method && isset( $package['rates'][ $chosen_method ] ) ) { + $rate = $package['rates'][ $chosen_method ]; // Merge cost and taxes - label and ID will be the same $this->shipping_total += $rate->cost; - foreach ( array_keys( $this->shipping_taxes + $rate->taxes ) as $key ) { - $this->shipping_taxes[ $key ] = ( isset( $rate->taxes[$key] ) ? $rate->taxes[$key] : 0 ) + ( isset( $this->shipping_taxes[$key] ) ? $this->shipping_taxes[$key] : 0 ); + if ( ! empty( $rate->taxes ) && is_array( $rate->taxes ) ) { + foreach ( array_keys( $this->shipping_taxes + $rate->taxes ) as $key ) { + $this->shipping_taxes[ $key ] = ( isset( $rate->taxes[ $key ] ) ? $rate->taxes[ $key ] : 0 ) + ( isset( $this->shipping_taxes[ $key ] ) ? $this->shipping_taxes[ $key ] : 0 ); + } } } } @@ -323,122 +319,93 @@ class WC_Shipping { } /** - * calculate_shipping_for_package function. - * - * Calculates each shipping methods cost. Rates are cached based on the package to speed up calculations. - * - * @access public - * @param array $package cart items - * @return array - * @todo Return array() instead of false for consistent return type? + * See if package is shippable. + * @param array $package + * @return boolean */ - public function calculate_shipping_for_package( $package = array() ) { - if ( ! $this->enabled ) return false; - if ( ! $package ) return false; + protected function is_package_shippable( $package ) { + $allowed = array_keys( WC()->countries->get_shipping_countries() ); + return in_array( $package['destination']['country'], $allowed ); + } + + /** + * Calculate shipping rates for a package, + * + * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load. + * + * @param array $package cart items + * @param int $package_key Index of the package being calculated. Used to cache multiple package rates. + * + * @return array|bool + */ + public function calculate_shipping_for_package( $package = array(), $package_key = 0 ) { + if ( ! $this->enabled || empty( $package ) || ! $this->is_package_shippable( $package ) ) { + return false; + } // Check if we need to recalculate shipping for this package - $package_hash = 'wc_ship_' . md5( json_encode( $package ) ); - $status_options = get_option( 'woocommerce_status_options', array() ); + $package_to_hash = $package; - if ( false === ( $stored_rates = get_transient( $package_hash ) ) || ( ! empty( $status_options['shipping_debug_mode'] ) && current_user_can( 'manage_options' ) ) ) { + // Remove data objects so hashes are consistent + foreach ( $package_to_hash['contents'] as $item_id => $item ) { + unset( $package_to_hash['contents'][ $item_id ]['data'] ); + } + $package_hash = 'wc_ship_' . md5( json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) ); + $session_key = 'shipping_for_package_' . $package_key; + $stored_rates = WC()->session->get( $session_key ); + + if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) { // Calculate shipping method rates $package['rates'] = array(); foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) { - - if ( $shipping_method->is_available( $package ) && ( empty( $package['ship_via'] ) || in_array( $shipping_method->id, $package['ship_via'] ) ) ) { - - // Reset Rates - $shipping_method->rates = array(); - - // Calculate Shipping for package - $shipping_method->calculate_shipping( $package ); - - // Place rates in package array - if ( ! empty( $shipping_method->rates ) && is_array( $shipping_method->rates ) ) - foreach ( $shipping_method->rates as $rate ) - $package['rates'][ $rate->id ] = $rate; + // Shipping instances need an ID + 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 } } // Filter the calculated rates $package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package ); - // Store - set_transient( $package_hash, $package['rates'], 60 * 60 ); // Cached for an hour - + // Store in session to avoid recalculation + WC()->session->set( $session_key, array( + 'package_hash' => $package_hash, + 'rates' => $package['rates'], + ) ); } else { - - $package['rates'] = $stored_rates; - + $package['rates'] = $stored_rates['rates']; } return $package; } /** - * Get packages + * Get packages. * @return array */ - public function get_packages() { + public function get_packages() { return $this->packages; } - /** - * reset_shipping function. + * Reset shipping. * * Reset the totals for shipping as a whole. - * - * @access public - * @return void */ public function reset_shipping() { unset( WC()->session->chosen_shipping_methods ); - $this->shipping_total = null; + $this->shipping_total = 0; $this->shipping_taxes = array(); $this->packages = array(); } - /** - * process_admin_options function. - * - * Saves options on the shipping setting page. - * - * @access public - * @return void + * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused. */ - public function process_admin_options() { - - $default_shipping_method = ( isset( $_POST['default_shipping_method'] ) ) ? esc_attr( $_POST['default_shipping_method'] ) : ''; - $method_order = ( isset( $_POST['method_order'] ) ) ? $_POST['method_order'] : ''; - - $order = array(); - - if ( is_array( $method_order ) && sizeof( $method_order ) > 0 ) { - $loop = 0; - foreach ($method_order as $method_id) { - $order[$method_id] = $loop; - $loop++; - } - } - - update_option( 'woocommerce_default_shipping_method', $default_shipping_method ); - update_option( 'woocommerce_shipping_method_order', $order ); + public function sort_shipping_methods() { + wc_deprecated_function( 'sort_shipping_methods', '2.6' ); + return $this->shipping_methods; } - } - -/** - * Register a shipping method - * - * Registers a shipping method ready to be loaded. Accepts a class name (string) or a class object. - * - * @package WooCommerce/Classes/Shipping - * @since 1.5.7 - */ -function woocommerce_register_shipping_method( $shipping_method ) { - $GLOBALS['woocommerce']->shipping->register_shipping_method( $shipping_method ); -} \ No newline at end of file diff --git a/includes/class-wc-shortcodes.php b/includes/class-wc-shortcodes.php index ada1b6f52ce..16675ac7c31 100644 --- a/includes/class-wc-shortcodes.php +++ b/includes/class-wc-shortcodes.php @@ -1,20 +1,24 @@ __CLASS__ . '::product', 'product_page' => __CLASS__ . '::product_page', @@ -46,10 +50,12 @@ class WC_Shortcodes { } /** - * Shortcode Wrapper + * Shortcode Wrapper. * - * @param mixed $function + * @param string[] $function * @param array $atts (default: array()) + * @param array $wrapper + * * @return string */ public static function shortcode_wrapper( @@ -58,36 +64,85 @@ class WC_Shortcodes { $wrapper = array( 'class' => 'woocommerce', 'before' => null, - 'after' => null + 'after' => null, ) ) { ob_start(); - $before = empty( $wrapper['before'] ) ? '
    ' : $wrapper['before']; - $after = empty( $wrapper['after'] ) ? '
    ' : $wrapper['after']; - - echo $before; + echo empty( $wrapper['before'] ) ? '
    ' : $wrapper['before']; call_user_func( $function, $atts ); - echo $after; + echo empty( $wrapper['after'] ) ? '
    ' : $wrapper['after']; return ob_get_clean(); } /** - * Cart page shortcode. - * - * @access public - * @param mixed $atts + * Loop over found products. + * @param array $query_args + * @param array $atts + * @param string $loop_name * @return string */ - public static function cart( $atts ) { - return self::shortcode_wrapper( array( 'WC_Shortcode_Cart', 'output' ), $atts ); + private static function product_loop( $query_args, $atts, $loop_name ) { + global $woocommerce_loop; + + $columns = absint( $atts['columns'] ); + $woocommerce_loop['columns'] = $columns; + $woocommerce_loop['name'] = $loop_name; + $query_args = apply_filters( 'woocommerce_shortcode_products_query', $query_args, $atts, $loop_name ); + $transient_name = 'wc_loop' . substr( md5( json_encode( $query_args ) . $loop_name ), 28 ) . WC_Cache_Helper::get_transient_version( 'product_query' ); + $products = get_transient( $transient_name ); + + if ( false === $products || ! is_a( $products, 'WP_Query' ) ) { + $products = new WP_Query( $query_args ); + set_transient( $transient_name, $products, DAY_IN_SECONDS * 30 ); + } + + ob_start(); + + if ( $products->have_posts() ) { + + // Prime caches before grabbing objects. + update_post_caches( $products->posts, array( 'product', 'product_variation' ) ); + ?> + + + + + + have_posts() ) : $products->the_post(); ?> + + + + + + + + + + ' . ob_get_clean() . '
    '; + } + + /** + * Cart page shortcode. + * + * @return string + */ + public static function cart() { + return is_null( WC()->cart ) ? '' : self::shortcode_wrapper( array( 'WC_Shortcode_Cart', 'output' ) ); } /** * Checkout page shortcode. * - * @access public * @param mixed $atts * @return string */ @@ -98,7 +153,6 @@ class WC_Shortcodes { /** * Order tracking page shortcode. * - * @access public * @param mixed $atts * @return string */ @@ -107,9 +161,8 @@ class WC_Shortcodes { } /** - * Cart shortcode. + * My account page shortcode. * - * @access public * @param mixed $atts * @return string */ @@ -118,83 +171,46 @@ class WC_Shortcodes { } /** - * List products in a category shortcode + * List products in a category shortcode. * - * @access public * @param array $atts * @return string */ public static function product_category( $atts ) { - global $woocommerce_loop; + $atts = shortcode_atts( array( + 'per_page' => '12', + 'columns' => '4', + 'orderby' => 'menu_order title', + 'order' => 'asc', + 'category' => '', // Slugs + 'operator' => 'IN', // Possible values are 'IN', 'NOT IN', 'AND'. + ), $atts, 'product_category' ); - if ( empty( $atts ) ) return ''; - - extract( shortcode_atts( array( - 'per_page' => '12', - 'columns' => '4', - 'orderby' => 'title', - 'order' => 'desc', - 'category' => '', - 'operator' => 'IN' // Possible values are 'IN', 'NOT IN', 'AND'. - ), $atts ) ); - - if ( ! $category ) return ''; - - // Default ordering args - $ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order ); - - $args = array( - 'post_type' => 'product', - 'post_status' => 'publish', - 'ignore_sticky_posts' => 1, - 'orderby' => $ordering_args['orderby'], - 'order' => $ordering_args['order'], - 'posts_per_page' => $per_page, - 'meta_query' => array( - array( - 'key' => '_visibility', - 'value' => array('catalog', 'visible'), - 'compare' => 'IN' - ) - ), - 'tax_query' => array( - array( - 'taxonomy' => 'product_cat', - 'terms' => array( esc_attr( $category ) ), - 'field' => 'slug', - 'operator' => $operator - ) - ) - ); - - if ( isset( $ordering_args['meta_key'] ) ) { - $args['meta_key'] = $ordering_args['meta_key']; + if ( ! $atts['category'] ) { + return ''; } - ob_start(); + // Default ordering args + $ordering_args = WC()->query->get_catalog_ordering_args( $atts['orderby'], $atts['order'] ); + $meta_query = WC()->query->get_meta_query(); + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => 1, + 'orderby' => $ordering_args['orderby'], + 'order' => $ordering_args['order'], + 'posts_per_page' => $atts['per_page'], + 'meta_query' => $meta_query, + 'tax_query' => WC()->query->get_tax_query(), + ); - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); + $query_args = self::_maybe_add_category_args( $query_args, $atts['category'], $atts['operator'] ); - $woocommerce_loop['columns'] = $columns; + if ( isset( $ordering_args['meta_key'] ) ) { + $query_args['meta_key'] = $ordering_args['meta_key']; + } - if ( $products->have_posts() ) : ?> - - - - have_posts() ) : $products->the_post(); ?> - - - - - - - - ' . ob_get_clean() . ''; + $return = self::product_loop( $query_args, $atts, 'product_cat' ); // Remove ordering query arguments WC()->query->remove_ordering_args(); @@ -204,82 +220,70 @@ class WC_Shortcodes { /** - * List all (or limited) product categories + * List all (or limited) product categories. * - * @access public * @param array $atts * @return string */ public static function product_categories( $atts ) { global $woocommerce_loop; - extract( shortcode_atts( array( + $atts = shortcode_atts( array( 'number' => null, 'orderby' => 'name', 'order' => 'ASC', - 'columns' => '4', + 'columns' => '4', 'hide_empty' => 1, - 'parent' => '' - ), $atts ) ); + 'parent' => '', + 'ids' => '', + ), $atts, 'product_categories' ); - if ( isset( $atts[ 'ids' ] ) ) { - $ids = explode( ',', $atts[ 'ids' ] ); - $ids = array_map( 'trim', $ids ); - } else { - $ids = array(); - } - - $hide_empty = ( $hide_empty == true || $hide_empty == 1 ) ? 1 : 0; + $ids = array_filter( array_map( 'trim', explode( ',', $atts['ids'] ) ) ); + $hide_empty = ( true === $atts['hide_empty'] || 'true' === $atts['hide_empty'] || 1 === $atts['hide_empty'] || '1' === $atts['hide_empty'] ) ? 1 : 0; // get terms and workaround WP bug with parents/pad counts $args = array( - 'orderby' => $orderby, - 'order' => $order, + 'orderby' => $atts['orderby'], + 'order' => $atts['order'], 'hide_empty' => $hide_empty, 'include' => $ids, 'pad_counts' => true, - 'child_of' => $parent + 'child_of' => $atts['parent'], ); $product_categories = get_terms( 'product_cat', $args ); - if ( $parent !== "" ) { - $product_categories = wp_list_filter( $product_categories, array( 'parent' => $parent ) ); + if ( '' !== $atts['parent'] ) { + $product_categories = wp_list_filter( $product_categories, array( 'parent' => $atts['parent'] ) ); } if ( $hide_empty ) { foreach ( $product_categories as $key => $category ) { - if ( $category->count == 0 ) { + if ( 0 == $category->count ) { unset( $product_categories[ $key ] ); } } } - if ( $number ) { - $product_categories = array_slice( $product_categories, 0, $number ); + if ( $atts['number'] ) { + $product_categories = array_slice( $product_categories, 0, $atts['number'] ); } + $columns = absint( $atts['columns'] ); $woocommerce_loop['columns'] = $columns; ob_start(); - // Reset loop/columns globals when starting a new loop - $woocommerce_loop['loop'] = $woocommerce_loop['column'] = ''; - if ( $product_categories ) { - woocommerce_product_loop_start(); foreach ( $product_categories as $category ) { - wc_get_template( 'content-product_cat.php', array( - 'category' => $category + 'category' => $category, ) ); - } woocommerce_product_loop_end(); - } woocommerce_reset_loop(); @@ -288,165 +292,105 @@ class WC_Shortcodes { } /** - * Recent Products shortcode + * Recent Products shortcode. * - * @access public * @param array $atts * @return string */ public static function recent_products( $atts ) { - global $woocommerce_loop; + $atts = shortcode_atts( array( + 'per_page' => '12', + 'columns' => '4', + 'orderby' => 'date', + 'order' => 'desc', + 'category' => '', // Slugs + 'operator' => 'IN', // Possible values are 'IN', 'NOT IN', 'AND'. + ), $atts, 'recent_products' ); - extract( shortcode_atts( array( - 'per_page' => '12', - 'columns' => '4', - 'orderby' => 'date', - 'order' => 'desc' - ), $atts ) ); - - $meta_query = WC()->query->get_meta_query(); - - $args = array( - 'post_type' => 'product', - 'post_status' => 'publish', - 'ignore_sticky_posts' => 1, - 'posts_per_page' => $per_page, - 'orderby' => $orderby, - 'order' => $order, - 'meta_query' => $meta_query + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => 1, + 'posts_per_page' => $atts['per_page'], + 'orderby' => $atts['orderby'], + 'order' => $atts['order'], + 'meta_query' => WC()->query->get_meta_query(), + 'tax_query' => WC()->query->get_tax_query(), ); - ob_start(); + $query_args = self::_maybe_add_category_args( $query_args, $atts['category'], $atts['operator'] ); - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); - - $woocommerce_loop['columns'] = $columns; - - if ( $products->have_posts() ) : ?> - - - - have_posts() ) : $products->the_post(); ?> - - - - - - - - ' . ob_get_clean() . ''; + return self::product_loop( $query_args, $atts, 'recent_products' ); } - /** - * List multiple products shortcode + * List multiple products shortcode. * - * @access public * @param array $atts * @return string */ public static function products( $atts ) { - global $woocommerce_loop; + $atts = shortcode_atts( array( + 'columns' => '4', + 'orderby' => 'title', + 'order' => 'asc', + 'ids' => '', + 'skus' => '', + ), $atts, 'products' ); - if ( empty( $atts ) ) return ''; - - extract( shortcode_atts( array( - 'columns' => '4', - 'orderby' => 'title', - 'order' => 'asc' - ), $atts ) ); - - $args = array( - 'post_type' => 'product', - 'post_status' => 'publish', - 'ignore_sticky_posts' => 1, - 'orderby' => $orderby, - 'order' => $order, - 'posts_per_page' => -1, - 'meta_query' => array( - array( - 'key' => '_visibility', - 'value' => array('catalog', 'visible'), - 'compare' => 'IN' - ) - ) + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => 1, + 'orderby' => $atts['orderby'], + 'order' => $atts['order'], + 'posts_per_page' => -1, + 'meta_query' => WC()->query->get_meta_query(), + 'tax_query' => WC()->query->get_tax_query(), ); - if ( isset( $atts['skus'] ) ) { - $skus = explode( ',', $atts['skus'] ); - $skus = array_map( 'trim', $skus ); - $args['meta_query'][] = array( - 'key' => '_sku', - 'value' => $skus, - 'compare' => 'IN' + if ( ! empty( $atts['skus'] ) ) { + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => array_map( 'trim', explode( ',', $atts['skus'] ) ), + 'compare' => 'IN', ); } - if ( isset( $atts['ids'] ) ) { - $ids = explode( ',', $atts['ids'] ); - $ids = array_map( 'trim', $ids ); - $args['post__in'] = $ids; + if ( ! empty( $atts['ids'] ) ) { + $query_args['post__in'] = array_map( 'trim', explode( ',', $atts['ids'] ) ); } - ob_start(); - - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); - - $woocommerce_loop['columns'] = $columns; - - if ( $products->have_posts() ) : ?> - - - - have_posts() ) : $products->the_post(); ?> - - - - - - - - ' . ob_get_clean() . ''; + return self::product_loop( $query_args, $atts, 'products' ); } - /** - * Display a single product + * Display a single product. * - * @access public * @param array $atts * @return string */ public static function product( $atts ) { - if ( empty( $atts ) ) return ''; + if ( empty( $atts ) ) { + return ''; + } + + $meta_query = WC()->query->get_meta_query(); $args = array( - 'post_type' => 'product', - 'posts_per_page' => 1, - 'no_found_rows' => 1, - 'post_status' => 'publish', - 'meta_query' => array( - array( - 'key' => '_visibility', - 'value' => array('catalog', 'visible'), - 'compare' => 'IN' - ) - ) + 'post_type' => 'product', + 'posts_per_page' => 1, + 'no_found_rows' => 1, + 'post_status' => 'publish', + 'meta_query' => $meta_query, + 'tax_query' => WC()->query->get_tax_query(), ); if ( isset( $atts['sku'] ) ) { $args['meta_query'][] = array( - 'key' => '_sku', - 'value' => $atts['sku'], - 'compare' => '=' + 'key' => '_sku', + 'value' => $atts['sku'], + 'compare' => '=', ); } @@ -456,7 +400,7 @@ class WC_Shortcodes { ob_start(); - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); + $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts, null ) ); if ( $products->have_posts() ) : ?> @@ -474,54 +418,63 @@ class WC_Shortcodes { wp_reset_postdata(); - return '
    ' . ob_get_clean() . '
    '; + $css_class = 'woocommerce'; + + if ( isset( $atts['class'] ) ) { + $css_class .= ' ' . $atts['class']; + } + + return '
    ' . ob_get_clean() . '
    '; } /** - * Display a single product price + cart button + * Display a single product price + cart button. * - * @access public * @param array $atts * @return string */ public static function product_add_to_cart( $atts ) { - global $wpdb, $post; + global $post; - if ( empty( $atts ) ) return ''; + if ( empty( $atts ) ) { + return ''; + } - extract( shortcode_atts( array( + $atts = shortcode_atts( array( 'id' => '', 'class' => '', - 'quantity' => '1', + 'quantity' => '1', 'sku' => '', 'style' => 'border:4px solid #ccc; padding: 12px;', - 'show_price' => 'true' - ), $atts ) ); + 'show_price' => 'true', + ), $atts, 'product_add_to_cart' ); - if ( ! empty( $id ) ) { - $product_data = get_post( $id ); - } elseif ( ! empty( $sku ) ) { - $product_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_sku' AND meta_value='%s' LIMIT 1", $sku ) ); + if ( ! empty( $atts['id'] ) ) { + $product_data = get_post( $atts['id'] ); + } elseif ( ! empty( $atts['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $atts['sku'] ); $product_data = get_post( $product_id ); } else { return ''; } - $product = wc_setup_product_data( $product_data ); + $product = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ) ) ? wc_setup_product_data( $product_data ) : false; if ( ! $product ) { return ''; } + $styles = empty( $atts['style'] ) ? '' : ' style="' . esc_attr( $atts['style'] ) . '"'; + ob_start(); ?> -

    +

    > - + get_price_html(); ?> - $quantity )); ?> + $atts['quantity'] ) ); ?>

    get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_sku' AND meta_value='%s' LIMIT 1", $atts['sku'] ) ); + $product_id = wc_get_product_id_by_sku( $atts['sku'] ); $product_data = get_post( $product_id ); } else { return ''; } - if ( 'product' !== $product_data->post_type ) { + $product = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ) ) ? wc_setup_product_data( $product_data ) : false; + + if ( ! $product ) { return ''; } - $_product = get_product( $product_data ); + $_product = wc_get_product( $product_data ); return esc_url( $_product->add_to_cart_url() ); } /** - * List all products on sale + * List all products on sale. * - * @access public * @param array $atts * @return string */ public static function sale_products( $atts ) { - global $woocommerce_loop; + $atts = shortcode_atts( array( + 'per_page' => '12', + 'columns' => '4', + 'orderby' => 'title', + 'order' => 'asc', + 'category' => '', // Slugs + 'operator' => 'IN', // Possible values are 'IN', 'NOT IN', 'AND'. + ), $atts, 'sale_products' ); - extract( shortcode_atts( array( - 'per_page' => '12', - 'columns' => '4', - 'orderby' => 'title', - 'order' => 'asc' - ), $atts ) ); - - // Get products on sale - $product_ids_on_sale = wc_get_product_ids_on_sale(); - - $meta_query = array(); - $meta_query[] = WC()->query->visibility_meta_query(); - $meta_query[] = WC()->query->stock_status_meta_query(); - $meta_query = array_filter( $meta_query ); - - $args = array( - 'posts_per_page' => $per_page, - 'orderby' => $orderby, - 'order' => $order, - 'no_found_rows' => 1, - 'post_status' => 'publish', - 'post_type' => 'product', - 'meta_query' => $meta_query, - 'post__in' => array_merge( array( 0 ), $product_ids_on_sale ) + $query_args = array( + 'posts_per_page' => $atts['per_page'], + 'orderby' => $atts['orderby'], + 'order' => $atts['order'], + 'no_found_rows' => 1, + 'post_status' => 'publish', + 'post_type' => 'product', + 'meta_query' => WC()->query->get_meta_query(), + 'tax_query' => WC()->query->get_tax_query(), + 'post__in' => array_merge( array( 0 ), wc_get_product_ids_on_sale() ), ); - ob_start(); + $query_args = self::_maybe_add_category_args( $query_args, $atts['category'], $atts['operator'] ); - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); - - $woocommerce_loop['columns'] = $columns; - - if ( $products->have_posts() ) : ?> - - - - have_posts() ) : $products->the_post(); ?> - - - - - - - - ' . ob_get_clean() . ''; + return self::product_loop( $query_args, $atts, 'sale_products' ); } /** - * List best selling products on sale + * List best selling products on sale. * - * @access public * @param array $atts * @return string */ public static function best_selling_products( $atts ) { - global $woocommerce_loop; + $atts = shortcode_atts( array( + 'per_page' => '12', + 'columns' => '4', + 'category' => '', // Slugs + 'operator' => 'IN', // Possible values are 'IN', 'NOT IN', 'AND'. + ), $atts, 'best_selling_products' ); - extract( shortcode_atts( array( - 'per_page' => '12', - 'columns' => '4' - ), $atts ) ); - - $args = array( - 'post_type' => 'product', - 'post_status' => 'publish', - 'ignore_sticky_posts' => 1, - 'posts_per_page' => $per_page, - 'meta_key' => 'total_sales', - 'orderby' => 'meta_value_num', - 'meta_query' => array( - array( - 'key' => '_visibility', - 'value' => array( 'catalog', 'visible' ), - 'compare' => 'IN' - ) - ) + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => 1, + 'posts_per_page' => $atts['per_page'], + 'meta_key' => 'total_sales', + 'orderby' => 'meta_value_num', + 'meta_query' => WC()->query->get_meta_query(), + 'tax_query' => WC()->query->get_tax_query(), ); - ob_start(); + $query_args = self::_maybe_add_category_args( $query_args, $atts['category'], $atts['operator'] ); - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); - - $woocommerce_loop['columns'] = $columns; - - if ( $products->have_posts() ) : ?> - - - - have_posts() ) : $products->the_post(); ?> - - - - - - - - ' . ob_get_clean() . ''; + return self::product_loop( $query_args, $atts, 'best_selling_products' ); } /** - * List top rated products on sale + * List top rated products on sale. * - * @access public * @param array $atts * @return string */ public static function top_rated_products( $atts ) { - global $woocommerce_loop; + $atts = shortcode_atts( array( + 'per_page' => '12', + 'columns' => '4', + 'orderby' => 'title', + 'order' => 'asc', + 'category' => '', // Slugs + 'operator' => 'IN', // Possible values are 'IN', 'NOT IN', 'AND'. + ), $atts, 'top_rated_products' ); - extract( shortcode_atts( array( - 'per_page' => '12', - 'columns' => '4', - 'orderby' => 'title', - 'order' => 'asc' - ), $atts ) ); - - $args = array( - 'post_type' => 'product', - 'post_status' => 'publish', - 'ignore_sticky_posts' => 1, - 'orderby' => $orderby, - 'order' => $order, - 'posts_per_page' => $per_page, - 'meta_query' => array( - array( - 'key' => '_visibility', - 'value' => array('catalog', 'visible'), - 'compare' => 'IN' - ) - ) + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => 1, + 'orderby' => $atts['orderby'], + 'order' => $atts['order'], + 'posts_per_page' => $atts['per_page'], + 'meta_query' => WC()->query->get_meta_query(), + 'tax_query' => WC()->query->get_tax_query(), ); - ob_start(); + $query_args = self::_maybe_add_category_args( $query_args, $atts['category'], $atts['operator'] ); add_filter( 'posts_clauses', array( __CLASS__, 'order_by_rating_post_clauses' ) ); - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); + $return = self::product_loop( $query_args, $atts, 'top_rated_products' ); remove_filter( 'posts_clauses', array( __CLASS__, 'order_by_rating_post_clauses' ) ); - $woocommerce_loop['columns'] = $columns; - - if ( $products->have_posts() ) : ?> - - - - have_posts() ) : $products->the_post(); ?> - - - - - - - - ' . ob_get_clean() . ''; + return $return; } /** - * Output featured products + * Output featured products. * - * @access public * @param array $atts * @return string */ public static function featured_products( $atts ) { - global $woocommerce_loop; + $atts = shortcode_atts( array( + 'per_page' => '12', + 'columns' => '4', + 'orderby' => 'date', + 'order' => 'desc', + 'category' => '', // Slugs + 'operator' => 'IN', // Possible values are 'IN', 'NOT IN', 'AND'. + ), $atts, 'featured_products' ); - extract( shortcode_atts( array( - 'per_page' => '12', - 'columns' => '4', - 'orderby' => 'date', - 'order' => 'desc' - ), $atts ) ); - - $args = array( - 'post_type' => 'product', - 'post_status' => 'publish', - 'ignore_sticky_posts' => 1, - 'posts_per_page' => $per_page, - 'orderby' => $orderby, - 'order' => $order, - 'meta_query' => array( - array( - 'key' => '_visibility', - 'value' => array('catalog', 'visible'), - 'compare' => 'IN' - ), - array( - 'key' => '_featured', - 'value' => 'yes' - ) - ) + $meta_query = WC()->query->get_meta_query(); + $tax_query = WC()->query->get_tax_query(); + $tax_query[] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + 'operator' => 'IN', ); - ob_start(); + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => 1, + 'posts_per_page' => $atts['per_page'], + 'orderby' => $atts['orderby'], + 'order' => $atts['order'], + 'meta_query' => $meta_query, + 'tax_query' => $tax_query, + ); - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); + $query_args = self::_maybe_add_category_args( $query_args, $atts['category'], $atts['operator'] ); - $woocommerce_loop['columns'] = $columns; - - if ( $products->have_posts() ) : ?> - - - - have_posts() ) : $products->the_post(); ?> - - - - - - - - ' . ob_get_clean() . ''; + return self::product_loop( $query_args, $atts, 'featured_products' ); } - /** - * Show a single product page + * Show a single product page. * - * @access public * @param array $atts * @return string */ public static function product_page( $atts ) { - if ( empty( $atts ) ) return ''; + if ( empty( $atts ) ) { + return ''; + } - if ( ! isset( $atts['id'] ) && ! isset( $atts['sku'] ) ) return ''; + if ( ! isset( $atts['id'] ) && ! isset( $atts['sku'] ) ) { + return ''; + } $args = array( - 'posts_per_page' => 1, - 'post_type' => 'product', - 'post_status' => 'publish', - 'ignore_sticky_posts' => 1, - 'no_found_rows' => 1 + 'posts_per_page' => 1, + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => 1, + 'no_found_rows' => 1, ); if ( isset( $atts['sku'] ) ) { $args['meta_query'][] = array( 'key' => '_sku', - 'value' => $atts['sku'], - 'compare' => '=' + 'value' => sanitize_text_field( $atts['sku'] ), + 'compare' => '=', ); + + $args['post_type'] = array( 'product', 'product_variation' ); } if ( isset( $atts['id'] ) ) { - $args['p'] = $atts['id']; + $args['p'] = absint( $atts['id'] ); } $single_product = new WP_Query( $args ); + $preselected_id = '0'; + + // check if sku is a variation + if ( isset( $atts['sku'] ) && $single_product->have_posts() && 'product_variation' === $single_product->post->post_type ) { + + $variation = new WC_Product_Variation( $single_product->post->ID ); + $attributes = $variation->get_attributes(); + + // set preselected id to be used by JS to provide context + $preselected_id = $single_product->post->ID; + + // get the parent product object + $args = array( + 'posts_per_page' => 1, + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => 1, + 'no_found_rows' => 1, + 'p' => $single_product->post->post_parent, + ); + + $single_product = new WP_Query( $args ); + ?> + + have_posts() ) : $single_product->the_post(); wp_enqueue_script( 'wc-single-product' ); ?> + global $wp_query; -
    + // Backup query object so following loops think this is a product page. + $previous_wp_query = $wp_query; + $wp_query = $single_product; + wp_enqueue_script( 'wc-single-product' ); + + while ( $single_product->have_posts() ) { + $single_product->the_post() + ?> +
    -
    + ' . ob_get_clean() . '
    '; } - /** - * Show messages + * Show messages. * - * @access public * @return string */ public static function shop_messages() { ob_start(); - wc_print_notices(); - return '
    ' . ob_get_clean() . '
    '; } /** * woocommerce_order_by_rating_post_clauses function. * - * @access public * @param array $args * @return array */ public static function order_by_rating_post_clauses( $args ) { global $wpdb; - $args['where'] .= " AND $wpdb->commentmeta.meta_key = 'rating' "; - - $args['join'] .= " - LEFT JOIN $wpdb->comments ON($wpdb->posts.ID = $wpdb->comments.comment_post_ID) - LEFT JOIN $wpdb->commentmeta ON($wpdb->comments.comment_ID = $wpdb->commentmeta.comment_id) - "; - + $args['where'] .= " AND $wpdb->commentmeta.meta_key = 'rating' "; + $args['join'] .= "LEFT JOIN $wpdb->comments ON($wpdb->posts.ID = $wpdb->comments.comment_post_ID) LEFT JOIN $wpdb->commentmeta ON($wpdb->comments.comment_ID = $wpdb->commentmeta.comment_id)"; $args['orderby'] = "$wpdb->commentmeta.meta_value DESC"; - $args['groupby'] = "$wpdb->posts.ID"; return $args; } - /** - * List products with an attribute shortcode - * Example [product_attribute attribute='color' filter='black'] + * List products with an attribute shortcode. + * Example [product_attribute attribute='color' filter='black']. * - * @access public * @param array $atts * @return string */ public static function product_attribute( $atts ) { - global $woocommerce_loop; - - extract( shortcode_atts( array( + $atts = shortcode_atts( array( 'per_page' => '12', 'columns' => '4', 'orderby' => 'title', 'order' => 'asc', 'attribute' => '', - 'filter' => '' - ), $atts ) ); + 'filter' => '', + ), $atts, 'product_attribute' ); - $attribute = strstr( $attribute, 'pa_' ) ? sanitize_title( $attribute ) : 'pa_' . sanitize_title( $attribute ); - - $args = array( + $query_args = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => 1, - 'posts_per_page' => $per_page, - 'orderby' => $orderby, - 'order' => $order, - 'meta_query' => array( - array( - 'key' => '_visibility', - 'value' => array('catalog', 'visible'), - 'compare' => 'IN' - ) - ), - 'tax_query' => array( - array( - 'taxonomy' => $attribute, - 'terms' => array_map( 'sanitize_title', explode( ",", $filter ) ), - 'field' => 'slug' - ) - ) + 'posts_per_page' => $atts['per_page'], + 'orderby' => $atts['orderby'], + 'order' => $atts['order'], + 'meta_query' => WC()->query->get_meta_query(), + 'tax_query' => WC()->query->get_tax_query(), ); - ob_start(); + $query_args['tax_query'][] = array( + 'taxonomy' => strstr( $atts['attribute'], 'pa_' ) ? sanitize_title( $atts['attribute'] ) : 'pa_' . sanitize_title( $atts['attribute'] ), + 'terms' => array_map( 'sanitize_title', explode( ',', $atts['filter'] ) ), + 'field' => 'slug', + ); - $products = new WP_Query( apply_filters( 'woocommerce_shortcode_products_query', $args, $atts ) ); - - $woocommerce_loop['columns'] = $columns; - - if ( $products->have_posts() ) : ?> - - - - have_posts() ) : $products->the_post(); ?> - - - - - - - - ' . ob_get_clean() . ''; + return self::product_loop( $query_args, $atts, 'product_attribute' ); } /** + * List related products. * @param array $atts * @return string */ public static function related_products( $atts ) { - $atts = shortcode_atts( array( - 'posts_per_page' => '2', - 'columns' => '2', - 'orderby' => 'rand', - ), $atts ); - - if ( isset( $atts['per_page'] ) ) { - _deprecated_argument( __CLASS__ . '->' . __FUNCTION__, '2.1', __( 'Use $args["posts_per_page"] instead. Deprecated argument will be removed in WC 2.2.', 'woocommerce' ) ); - $atts['posts_per_page'] = $atts['per_page']; - unset( $atts['per_page'] ); - } + 'per_page' => '4', + 'columns' => '4', + 'orderby' => 'rand', + ), $atts, 'related_products' ); ob_start(); + // Rename arg + $atts['posts_per_page'] = absint( $atts['per_page'] ); + woocommerce_related_products( $atts ); return ob_get_clean(); } + + /** + * Adds a tax_query index to the query to filter by category. + * + * @param array $args + * @param string $category + * @param string $operator + * @return array; + * @access private + */ + private static function _maybe_add_category_args( $args, $category, $operator ) { + if ( ! empty( $category ) ) { + if ( empty( $args['tax_query'] ) ) { + $args['tax_query'] = array(); + } + $args['tax_query'][] = array( + array( + 'taxonomy' => 'product_cat', + 'terms' => array_map( 'sanitize_title', explode( ',', $category ) ), + 'field' => 'slug', + 'operator' => $operator, + ), + ); + } + + return $args; + } } diff --git a/includes/class-wc-structured-data.php b/includes/class-wc-structured-data.php new file mode 100644 index 00000000000..d7cdd0b24c1 --- /dev/null +++ b/includes/class-wc-structured-data.php @@ -0,0 +1,449 @@ + + */ +class WC_Structured_Data { + + /** + * @var array $_data + */ + private $_data = array(); + + /** + * Constructor. + */ + public function __construct() { + // Generate structured data. + add_action( 'woocommerce_before_main_content', array( $this, 'generate_website_data' ), 30 ); + add_action( 'woocommerce_breadcrumb', array( $this, 'generate_breadcrumblist_data' ), 10 ); + add_action( 'woocommerce_shop_loop', array( $this, 'generate_product_data' ), 10 ); + add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ), 60 ); + add_action( 'woocommerce_review_meta', array( $this, 'generate_review_data' ), 20 ); + add_action( 'woocommerce_email_order_details', array( $this, 'generate_order_data' ), 20, 3 ); + + // Output structured data. + add_action( 'woocommerce_email_order_details', array( $this, 'output_email_structured_data' ), 30, 3 ); + add_action( 'wp_footer', array( $this, 'output_structured_data' ), 10 ); + } + + /** + * Sets data. + * + * @param array $data Structured data. + * @param bool $reset Unset data (default: false). + * @return bool + */ + public function set_data( $data, $reset = false ) { + if ( ! isset( $data['@type'] ) || ! preg_match( '|^[a-zA-Z]{1,20}$|', $data['@type'] ) ) { + return false; + } + + if ( $reset && isset( $this->_data ) ) { + unset( $this->_data ); + } + + $this->_data[] = $data; + + return true; + } + + /** + * Gets data. + * + * @return array + */ + public function get_data() { + return $this->_data; + } + + /** + * Structures and returns data. + * + * List of types available by default for specific request: + * + * 'product', + * 'review', + * 'breadcrumblist', + * 'website', + * 'order', + * + * @param array $types Structured data types. + * @return array + */ + public function get_structured_data( $types ) { + $data = array(); + + // Put together the values of same type of structured data. + foreach ( $this->get_data() as $value ) { + $data[ strtolower( $value['@type'] ) ][] = $value; + } + + // Wrap the multiple values of each type inside a graph... Then add context to each type. + foreach ( $data as $type => $value ) { + $data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0]; + $data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ]; + } + + // If requested types, pick them up... Finally change the associative array to an indexed one. + $data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data ); + + if ( ! empty( $data ) ) { + $data = count( $data ) > 1 ? array( '@graph' => $data ) : $data[0]; + } + + return $data; + } + + /** + * Get data types for pages. + * + * @return array + */ + protected function get_data_type_for_page() { + $types = array(); + $types[] = is_shop() || is_product_category() || is_product() ? 'product' : ''; + $types[] = is_shop() && is_front_page() ? 'website' : ''; + $types[] = is_product() ? 'review' : ''; + $types[] = ! is_shop() ? 'breadcrumblist' : ''; + $types[] = 'order'; + + return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) ); + } + + /** + * Makes sure email structured data only outputs on non-plain text versions. + * + * @param WP_Order $order Order data. + * @param bool $sent_to_admin Send to admin (default: false). + * @param bool $plain_text Plain text email (default: false). + */ + public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) { + if ( $plain_text ) { + return; + } + echo '
    '; + $this->output_structured_data(); + echo '
    '; + } + + /** + * Sanitizes, encodes and outputs structured data. + * + * Hooked into `wp_footer` action hook. + * Hooked into `woocommerce_email_order_details` action hook. + */ + public function output_structured_data() { + $types = $this->get_data_type_for_page(); + + if ( $data = wc_clean( $this->get_structured_data( $types ) ) ) { + echo ''; + } + } + + /* + |-------------------------------------------------------------------------- + | Generators + |-------------------------------------------------------------------------- + | + | Methods for generating specific structured data types: + | + | - Product + | - Review + | - BreadcrumbList + | - WebSite + | - Order + | + | The generated data is stored into `$this->_data`. + | See the methods above for handling `$this->_data`. + | + */ + + /** + * Generates Product structured data. + * + * Hooked into `woocommerce_single_product_summary` action hook. + * Hooked into `woocommerce_shop_loop` action hook. + * + * @param WC_Product $product Product data (default: null). + */ + public function generate_product_data( $product = null ) { + if ( ! is_object( $product ) ) { + global $product; + } + + if ( ! is_a( $product, 'WC_Product' ) ) { + return; + } + + $shop_name = get_bloginfo( 'name' ); + $shop_url = home_url(); + $currency = get_woocommerce_currency(); + $markup = array(); + $markup['@type'] = 'Product'; + $markup['@id'] = get_permalink( $product->get_id() ); + $markup['url'] = $markup['@id']; + $markup['name'] = $product->get_name(); + + if ( apply_filters( 'woocommerce_structured_data_product_limit', is_product_taxonomy() || is_shop() ) ) { + $this->set_data( apply_filters( 'woocommerce_structured_data_product_limited', $markup, $product ) ); + return; + } + + if ( '' !== $product->get_price() ) { + $markup_offer = array( + '@type' => 'Offer', + 'priceCurrency' => $currency, + 'availability' => 'https://schema.org/' . $stock = ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ), + 'sku' => $product->get_sku(), + 'image' => wp_get_attachment_url( $product->get_image_id() ), + 'description' => $product->get_description(), + 'seller' => array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ), + ); + + if ( $product->is_type( 'variable' ) ) { + $prices = $product->get_variation_prices(); + + if ( current( $prices['price'] ) === end( $prices['price'] ) ) { + $markup_offer['price'] = wc_format_decimal( $product->get_price(), wc_get_price_decimals() ); + } else { + $markup_offer['priceSpecification'] = array( + 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), + 'minPrice' => wc_format_decimal( current( $prices['price'] ), wc_get_price_decimals() ), + 'maxPrice' => wc_format_decimal( end( $prices['price'] ), wc_get_price_decimals() ), + 'priceCurrency' => $currency, + ); + } + } else { + $markup_offer['price'] = wc_format_decimal( $product->get_price(), wc_get_price_decimals() ); + } + + $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) ); + } + + if ( $product->get_rating_count() ) { + $markup['aggregateRating'] = array( + '@type' => 'AggregateRating', + 'ratingValue' => $product->get_average_rating(), + 'ratingCount' => $product->get_rating_count(), + 'reviewCount' => $product->get_review_count(), + ); + } + + $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) ); + } + + /** + * Generates Review structured data. + * + * Hooked into `woocommerce_review_meta` action hook. + * + * @param WP_Comment $comment Comment data. + */ + public function generate_review_data( $comment ) { + $markup = array(); + $markup['@type'] = 'Review'; + $markup['@id'] = get_comment_link( $comment->comment_ID ); + $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID ); + $markup['description'] = get_comment_text( $comment->comment_ID ); + $markup['itemReviewed'] = array( + '@type' => 'Product', + 'name' => get_the_title( $comment->comment_post_ID ), + ); + if ( $rating = get_comment_meta( $comment->comment_ID, 'rating', true ) ) { + $markup['reviewRating'] = array( + '@type' => 'rating', + 'ratingValue' => $rating, + ); + + // Skip replies unless they have a rating. + } elseif ( $comment->comment_parent ) { + return; + } + + $markup['author'] = array( + '@type' => 'Person', + 'name' => get_comment_author( $comment->comment_ID ), + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) ); + } + + /** + * Generates BreadcrumbList structured data. + * + * Hooked into `woocommerce_breadcrumb` action hook. + * + * @param WC_Breadcrumb $breadcrumbs Breadcrumb data. + */ + public function generate_breadcrumblist_data( $breadcrumbs ) { + $crumbs = $breadcrumbs->get_breadcrumb(); + + if ( empty( $crumbs ) || ! is_array( $crumbs ) ) { + return; + } + + $markup = array(); + $markup['@type'] = 'BreadcrumbList'; + $markup['itemListElement'] = array(); + + foreach ( $crumbs as $key => $crumb ) { + $markup['itemListElement'][ $key ] = array( + '@type' => 'ListItem', + 'position' => $key + 1, + 'item' => array( + 'name' => $crumb[0], + ), + ); + + if ( ! empty( $crumb[1] ) && sizeof( $crumbs ) !== $key + 1 ) { + $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] ); + } + } + + $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) ); + } + + /** + * Generates WebSite structured data. + * + * Hooked into `woocommerce_before_main_content` action hook. + */ + public function generate_website_data() { + $markup = array(); + $markup['@type'] = 'WebSite'; + $markup['name'] = get_bloginfo( 'name' ); + $markup['url'] = home_url(); + $markup['potentialAction'] = array( + '@type' => 'SearchAction', + 'target' => home_url( '?s={search_term_string}&post_type=product' ), + 'query-input' => 'required name=search_term_string', + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) ); + } + + /** + * Generates Order structured data. + * + * Hooked into `woocommerce_email_order_details` action hook. + * + * @param WP_Order $order Order data. + * @param bool $sent_to_admin Send to admin (default: false). + * @param bool $plain_text Plain text email (default: false). + */ + public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) { + if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) { + return; + } + + $shop_name = get_bloginfo( 'name' ); + $shop_url = home_url(); + $order_url = $sent_to_admin ? admin_url( 'post.php?post=' . absint( $order->get_id() ) . '&action=edit' ) : $order->get_view_order_url(); + $order_statuses = array( + 'pending' => 'https://schema.org/OrderPaymentDue', + 'processing' => 'https://schema.org/OrderProcessing', + 'on-hold' => 'https://schema.org/OrderProblem', + 'completed' => 'https://schema.org/OrderDelivered', + 'cancelled' => 'https://schema.org/OrderCancelled', + 'refunded' => 'https://schema.org/OrderReturned', + 'failed' => 'https://schema.org/OrderProblem', + ); + + $markup_offers = array(); + foreach ( $order->get_items() as $item ) { + if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { + continue; + } + + $product = apply_filters( 'woocommerce_order_item_product', $order->get_product_from_item( $item ), $item ); + $product_exists = is_object( $product ); + $is_visible = $product_exists && $product->is_visible(); + + $markup_offers[] = array( + '@type' => 'Offer', + 'price' => $order->get_line_subtotal( $item ), + 'priceCurrency' => $order->get_currency(), + 'priceSpecification' => array( + 'price' => $order->get_line_subtotal( $item ), + 'priceCurrency' => $order->get_currency(), + 'eligibleQuantity' => array( + '@type' => 'QuantitativeValue', + 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item['qty'], $item ), + ), + ), + 'itemOffered' => array( + '@type' => 'Product', + 'name' => apply_filters( 'woocommerce_order_item_name', $item['name'], $item, $is_visible ), + 'sku' => $product_exists ? $product->get_sku() : '', + 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '', + 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(), + ), + 'seller' => array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ), + ); + } + + $markup = array(); + $markup['@type'] = 'Order'; + $markup['url'] = $order_url; + $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : ''; + $markup['orderNumber'] = $order->get_order_number(); + $markup['orderDate'] = $order->get_date_created()->format( 'c' ); + $markup['acceptedOffer'] = $markup_offers; + $markup['discount'] = $order->get_total_discount(); + $markup['discountCurrency'] = $order->get_currency(); + $markup['price'] = $order->get_total(); + $markup['priceCurrency'] = $order->get_currency(); + $markup['priceSpecification'] = array( + 'price' => $order->get_total(), + 'priceCurrency' => $order->get_currency(), + 'valueAddedTaxIncluded' => true, + ); + $markup['billingAddress'] = array( + '@type' => 'PostalAddress', + 'name' => $order->get_formatted_billing_full_name(), + 'streetAddress' => $order->get_billing_address_1(), + 'postalCode' => $order->get_billing_postcode(), + 'addressLocality' => $order->get_billing_city(), + 'addressRegion' => $order->get_billing_state(), + 'addressCountry' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'telephone' => $order->get_billing_phone(), + ); + $markup['customer'] = array( + '@type' => 'Person', + 'name' => $order->get_formatted_billing_full_name(), + ); + $markup['merchant'] = array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ); + $markup['potentialAction'] = array( + '@type' => 'ViewAction', + 'name' => 'View Order', + 'url' => $order_url, + 'target' => $order_url, + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true ); + } +} diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php index 3377fda77eb..2325f0e9cc9 100644 --- a/includes/class-wc-tax.php +++ b/includes/class-wc-tax.php @@ -1,6 +1,11 @@ query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $removed_tax_class ) ); + $wpdb->query( "DELETE locations FROM {$wpdb->prefix}woocommerce_tax_rate_locations locations LEFT JOIN {$wpdb->prefix}woocommerce_tax_rates rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_id IS NULL;" ); + } + + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + } + } + + /** + * Calculate tax for a line. * @param float $price Price to calc tax on * @param array $rates Rates to apply * @param boolean $price_includes_tax Whether the passed price has taxes included @@ -35,10 +74,11 @@ class WC_Tax { // Work in pence to X precision $price = self::precision( $price ); - if ( $price_includes_tax ) + if ( $price_includes_tax ) { $taxes = self::calc_inclusive_tax( $price, $rates ); - else + } else { $taxes = self::calc_exclusive_tax( $price, $rates ); + } // Round to precision if ( ! self::$round_at_subtotal && ! $suppress_rounding ) { @@ -60,11 +100,12 @@ class WC_Tax { * @return array */ public static function calc_shipping_tax( $price, $rates ) { - return self::calc_exclusive_tax( $price, $rates ); + $taxes = self::calc_exclusive_tax( $price, $rates ); + return apply_filters( 'woocommerce_calc_shipping_tax', $taxes, $price, $rates ); } /** - * Multiply cost by pow precision + * Multiply cost by pow precision. * @param float $price * @return float */ @@ -73,7 +114,7 @@ class WC_Tax { } /** - * Divide cost by pow precision + * Divide cost by pow precision. * @param float $price * @return float */ @@ -86,44 +127,51 @@ class WC_Tax { * * Filter example: to return rounding to .5 cents you'd use: * - * public function euro_5cent_rounding( $in ) { + * function euro_5cent_rounding( $in ) { * return round( $in / 5, 2 ) * 5; * } * add_filter( 'woocommerce_tax_round', 'euro_5cent_rounding' ); + * + * @param float|int $in + * + * @return float */ public static function round( $in ) { return apply_filters( 'woocommerce_tax_round', round( $in, self::$precision ), $in ); } /** - * Calc tax from inclusive price + * Calc tax from inclusive price. * * @param float $price * @param array $rates * @return array */ - private static function calc_inclusive_tax( $price, $rates ) { + public static function calc_inclusive_tax( $price, $rates ) { $taxes = array(); $regular_tax_rates = $compound_tax_rates = 0; - foreach ( $rates as $key => $rate ) - if ( $rate['compound'] == 'yes' ) + foreach ( $rates as $key => $rate ) { + if ( 'yes' === $rate['compound'] ) { $compound_tax_rates = $compound_tax_rates + $rate['rate']; - else + } else { $regular_tax_rates = $regular_tax_rates + $rate['rate']; + } + } $regular_tax_rate = 1 + ( $regular_tax_rates / 100 ); $compound_tax_rate = 1 + ( $compound_tax_rates / 100 ); $non_compound_price = $price / $compound_tax_rate; foreach ( $rates as $key => $rate ) { - if ( ! isset( $taxes[ $key ] ) ) + if ( ! isset( $taxes[ $key ] ) ) { $taxes[ $key ] = 0; + } $the_rate = $rate['rate'] / 100; - if ( $rate['compound'] == 'yes' ) { + if ( 'yes' === $rate['compound'] ) { $the_price = $price; $the_rate = $the_rate / $compound_tax_rate; } else { @@ -140,41 +188,44 @@ class WC_Tax { } /** - * Calc tax from exclusive price + * Calc tax from exclusive price. * * @param float $price * @param array $rates * @return array */ - private static function calc_exclusive_tax( $price, $rates ) { + public static function calc_exclusive_tax( $price, $rates ) { $taxes = array(); - // Multiple taxes - foreach ( $rates as $key => $rate ) { - - if ( $rate['compound'] == 'yes' ) - continue; - - $tax_amount = $price * ( $rate['rate'] / 100 ); - - // ADVANCED: Allow third parties to modify this rate - $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); - - // Add rate - if ( ! isset( $taxes[ $key ] ) ) - $taxes[ $key ] = $tax_amount; - else - $taxes[ $key ] += $tax_amount; - } - - $pre_compound_total = array_sum( $taxes ); - - // Compound taxes - if ( $rates ) { + if ( ! empty( $rates ) ) { + // Multiple taxes foreach ( $rates as $key => $rate ) { - if ( $rate['compound'] == 'no' ) + if ( 'yes' === $rate['compound'] ) { continue; + } + + $tax_amount = $price * ( $rate['rate'] / 100 ); + + // ADVANCED: Allow third parties to modify this rate + $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); + + // Add rate + if ( ! isset( $taxes[ $key ] ) ) { + $taxes[ $key ] = $tax_amount; + } else { + $taxes[ $key ] += $tax_amount; + } + } + + $pre_compound_total = array_sum( $taxes ); + + // Compound taxes + foreach ( $rates as $key => $rate ) { + + if ( 'no' === $rate['compound'] ) { + continue; + } $the_price_inc_tax = $price + ( $pre_compound_total ); @@ -184,10 +235,11 @@ class WC_Tax { $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total ); // Add rate - if ( ! isset( $taxes[ $key ] ) ) + if ( ! isset( $taxes[ $key ] ) ) { $taxes[ $key ] = $tax_amount; - else + } else { $taxes[ $key ] += $tax_amount; + } } } @@ -197,22 +249,17 @@ class WC_Tax { /** * Searches for all matching country/state/postcode tax rates. * - * @access public * @param array $args * @return array */ public static function find_rates( $args = array() ) { - global $wpdb; - - $defaults = array( - 'country' => '', - 'state' => '', - 'city' => '', - 'postcode' => '', - 'tax_class' => '' - ); - - $args = wp_parse_args( $args, $defaults ); + $args = wp_parse_args( $args, array( + 'country' => '', + 'state' => '', + 'city' => '', + 'postcode' => '', + 'tax_class' => '', + ) ); extract( $args, EXTR_SKIP ); @@ -220,93 +267,208 @@ class WC_Tax { return array(); } - // Handle postcodes - $valid_postcodes = array( '*', strtoupper( wc_clean( $postcode ) ) ); - - // Work out possible valid wildcard postcodes - $postcode_length = strlen( $postcode ); - $wildcard_postcode = strtoupper( wc_clean( $postcode ) ); - - for ( $i = 0; $i < $postcode_length; $i ++ ) { - - $wildcard_postcode = substr( $wildcard_postcode, 0, -1 ); - - $valid_postcodes[] = $wildcard_postcode . '*'; - } - - // Build transient key and try to retrieve them from cache - $rates_transient_key = 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, implode( ',', $valid_postcodes), $tax_class ) ); - $matched_tax_rates = get_transient( $rates_transient_key ); + $postcode = wc_normalize_postcode( wc_clean( $postcode ) ); + $cache_key = WC_Cache_Helper::get_cache_prefix( 'taxes' ) . 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, $postcode, $tax_class ) ); + $matched_tax_rates = wp_cache_get( $cache_key, 'taxes' ); if ( false === $matched_tax_rates ) { - - // Run the query - $found_rates = $wpdb->get_results( $wpdb->prepare( " - SELECT tax_rates.* - FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates - LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id - LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id - WHERE tax_rate_country IN ( %s, '' ) - AND tax_rate_state IN ( %s, '' ) - AND tax_rate_class = %s - AND ( - locations.location_type IS NULL - OR ( - locations.location_type = 'postcode' - AND locations.location_code IN ('" . implode( "','", $valid_postcodes ) . "') - AND ( - locations2.location_type = 'city' AND locations2.location_code = %s - OR 0 = ( - SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sublocations - WHERE sublocations.location_type = 'city' - AND sublocations.tax_rate_id = tax_rates.tax_rate_id - ) - ) - ) - OR ( - locations.location_type = 'city' - AND locations.location_code = %s - AND 0 = ( - SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sublocations - WHERE sublocations.location_type = 'postcode' - AND sublocations.tax_rate_id = tax_rates.tax_rate_id - ) - ) - ) - GROUP BY tax_rate_id - ORDER BY tax_rate_priority, tax_rate_order - ", - strtoupper( $country ), - strtoupper( $state ), - sanitize_title( $tax_class ), - strtoupper( $city ), - strtoupper( $city ) - ) ); - - $matched_tax_rates = array(); - $found_priority = array(); - - foreach ( $found_rates as $found_rate ) { - if ( in_array( $found_rate->tax_rate_priority, $found_priority ) ) { - continue; - } - - $matched_tax_rates[ $found_rate->tax_rate_id ] = array( - 'rate' => $found_rate->tax_rate, - 'label' => $found_rate->tax_rate_name, - 'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no', - 'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no' - ); - - $found_priority[] = $found_rate->tax_rate_priority; - } - - $matched_tax_rates = apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ); - - set_transient( $rates_transient_key, $matched_tax_rates, DAY_IN_SECONDS ); + $matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ); + wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' ); } - return $matched_tax_rates; + return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args ); + } + + /** + * Searches for all matching country/state/postcode tax rates. + * + * @param array $args + * @return array + */ + public static function find_shipping_rates( $args = array() ) { + $rates = self::find_rates( $args ); + $shipping_rates = array(); + + if ( is_array( $rates ) ) { + foreach ( $rates as $key => $rate ) { + if ( 'yes' === $rate['shipping'] ) { + $shipping_rates[ $key ] = $rate; + } + } + } + + return $shipping_rates; + } + + /** + * Does the sort comparison. + * + * @param object $rate1 + * @param object $rate2 + * + * @return int + */ + private static function sort_rates_callback( $rate1, $rate2 ) { + if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) { + return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC + } elseif ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) { + if ( '' === $rate1->tax_rate_country ) { + return 1; + } + if ( '' === $rate2->tax_rate_country ) { + return -1; + } + return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1; + } elseif ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) { + if ( '' === $rate1->tax_rate_state ) { + return 1; + } + if ( '' === $rate2->tax_rate_state ) { + return -1; + } + return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1; + } else { + return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1; // Identical - use ID + } + } + + /** + * Logical sort order for tax rates based on the following in order of priority: + * - Priority + * - County code + * - State code + * @param array $rates + * @return array + */ + private static function sort_rates( $rates ) { + uasort( $rates, __CLASS__ . '::sort_rates_callback' ); + $i = 0; + foreach ( $rates as $key => $rate ) { + $rates[ $key ]->tax_rate_order = $i++; + } + return $rates; + } + + /** + * Loop through a set of tax rates and get the matching rates (1 per priority). + * + * @param string $country + * @param string $state + * @param string $postcode + * @param string $city + * @param string $tax_class + * @return array + */ + private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) { + global $wpdb; + + // Query criteria - these will be ANDed + $criteria = array(); + $criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) ); + $criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) ); + $criteria[] = $wpdb->prepare( "tax_rate_class = %s", sanitize_title( $tax_class ) ); + + // Pre-query postcode ranges for PHP based matching. + $postcode_search = wc_get_wildcard_postcodes( $postcode, $country ); + $postcode_ranges = $wpdb->get_results( "SELECT tax_rate_id, location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type = 'postcode' AND location_code LIKE '%...%';" ); + + if ( $postcode_ranges ) { + $matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country ); + if ( ! empty( $matches ) ) { + foreach ( $matches as $matched_postcodes ) { + $postcode_search = array_merge( $postcode_search, $matched_postcodes ); + } + } + } + + $postcode_search = array_unique( $postcode_search ); + + /** + * Location matching criteria - ORed + * Needs to match: + * - rates with no postcodes and cities + * - rates with a matching postcode and city + * - rates with matching postcode, no city + * - rates with matching city, no postcode + */ + $locations_criteria = array(); + $locations_criteria[] = "locations.location_type IS NULL"; + $locations_criteria[] = " + locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "') + AND ( + ( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' ) + OR NOT EXISTS ( + SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub + WHERE sub.location_type = 'city' + AND sub.tax_rate_id = tax_rates.tax_rate_id + ) + ) + "; + $locations_criteria[] = " + locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "' + AND NOT EXISTS ( + SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub + WHERE sub.location_type = 'postcode' + AND sub.tax_rate_id = tax_rates.tax_rate_id + ) + "; + $criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )'; + + $found_rates = $wpdb->get_results( " + SELECT tax_rates.* + FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id + WHERE 1=1 AND " . implode( ' AND ', $criteria ) . " + GROUP BY tax_rates.tax_rate_id + ORDER BY tax_rates.tax_rate_priority + " ); + + $found_rates = self::sort_rates( $found_rates ); + $matched_tax_rates = array(); + $found_priority = array(); + + foreach ( $found_rates as $found_rate ) { + if ( in_array( $found_rate->tax_rate_priority, $found_priority ) ) { + continue; + } + + $matched_tax_rates[ $found_rate->tax_rate_id ] = array( + 'rate' => $found_rate->tax_rate, + 'label' => $found_rate->tax_rate_name, + 'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no', + 'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no', + ); + + $found_priority[] = $found_rate->tax_rate_priority; + } + + return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ); + } + + /** + * Get the customer tax location based on their status and the current page. + * + * Used by get_rates(), get_shipping_rates(). + * + * @param $tax_class string Optional, passed to the filter for advanced tax setups. + * @return array + */ + public static function get_tax_location( $tax_class = '' ) { + $location = array(); + + if ( ! empty( WC()->customer ) ) { + $location = WC()->customer->get_taxable_address(); + } elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) { + $location = array( + WC()->countries->get_base_country(), + WC()->countries->get_base_state(), + WC()->countries->get_base_postcode(), + WC()->countries->get_base_city(), + ); + } + + return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class ); } /** @@ -315,34 +477,23 @@ class WC_Tax { * @return array */ public static function get_rates( $tax_class = '' ) { + $tax_class = sanitize_title( $tax_class ); + $location = self::get_tax_location( $tax_class ); + $matched_tax_rates = array(); - $tax_class = sanitize_title( $tax_class ); - - /* Checkout uses customer location for the tax rates. Also, if shipping has been calculated, use the customers address. */ - if ( ( defined('WOOCOMMERCE_CHECKOUT') && WOOCOMMERCE_CHECKOUT ) || ( ! empty( WC()->customer ) && WC()->customer->has_calculated_shipping() ) ) { - - list( $country, $state, $postcode, $city ) = WC()->customer->get_taxable_address(); + if ( sizeof( $location ) === 4 ) { + list( $country, $state, $postcode, $city ) = $location; $matched_tax_rates = self::find_rates( array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, - 'tax_class' => $tax_class + 'tax_class' => $tax_class, ) ); - - } else { - - // Prices which include tax should always use the base rate if we don't know where the user is located - // Prices excluding tax however should just not add any taxes, as they will be added during checkout. - // The woocommerce_default_customer_address option (when set to base) is also used here. - $matched_tax_rates = get_option( 'woocommerce_prices_include_tax' ) == 'yes' || get_option( 'woocommerce_default_customer_address' ) == 'base' - ? self::get_shop_base_rate( $tax_class ) - : array(); - } - return apply_filters('woocommerce_matched_rates', $matched_tax_rates, $tax_class); + return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class ); } /** @@ -351,14 +502,25 @@ class WC_Tax { * @param string Tax Class * @return array */ - public static function get_shop_base_rate( $tax_class = '' ) { - return self::find_rates( array( + public static function get_base_tax_rates( $tax_class = '' ) { + return apply_filters( 'woocommerce_base_tax_rates', self::find_rates( array( 'country' => WC()->countries->get_base_country(), 'state' => WC()->countries->get_base_state(), 'postcode' => WC()->countries->get_base_postcode(), 'city' => WC()->countries->get_base_city(), - 'tax_class' => $tax_class - ) ); + 'tax_class' => $tax_class, + ) ), $tax_class ); + } + + /** + * Alias for get_base_tax_rates(). + * + * @deprecated 2.3 + * @param string Tax Class + * @return array + */ + public static function get_shop_base_rate( $tax_class = '' ) { + return self::get_base_tax_rates( $tax_class ); } /** @@ -368,193 +530,171 @@ class WC_Tax { * @return mixed */ public static function get_shipping_tax_rates( $tax_class = null ) { - // See if we have an explicitly set shipping tax class - if ( $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ) ) { - $tax_class = $shipping_tax_class == 'standard' ? '' : $shipping_tax_class; + $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); + + if ( 'inherit' !== $shipping_tax_class ) { + $tax_class = $shipping_tax_class; } - if ( ( defined('WOOCOMMERCE_CHECKOUT') && WOOCOMMERCE_CHECKOUT ) || ( ! empty( WC()->customer ) && WC()->customer->has_calculated_shipping() ) ) { + $location = self::get_tax_location( $tax_class ); + $matched_tax_rates = array(); - list( $country, $state, $postcode, $city ) = WC()->customer->get_taxable_address(); + if ( sizeof( $location ) === 4 ) { + list( $country, $state, $postcode, $city ) = $location; - } else { - - // Prices which include tax should always use the base rate if we don't know where the user is located - // Prices excluding tax however should just not add any taxes, as they will be added during checkout - if ( get_option( 'woocommerce_prices_include_tax' ) == 'yes' || get_option( 'woocommerce_default_customer_address' ) == 'base' ) { - $country = WC()->countries->get_base_country(); - $state = WC()->countries->get_base_state(); - $postcode = ''; - $city = ''; - } else { - return array(); - } - - } - - // If we are here then shipping is taxable - work it out - if ( ! is_null( $tax_class ) ) { - - $matched_tax_rates = array(); - - // This will be per item shipping - $rates = self::find_rates( array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - 'tax_class' => $tax_class - ) ); - - if ( $rates ) - foreach ( $rates as $key => $rate ) - if ( isset( $rate['shipping'] ) && $rate['shipping'] == 'yes' ) - $matched_tax_rates[ $key ] = $rate; - - if ( sizeof( $matched_tax_rates ) == 0 ) { - // Get standard rate - $rates = self::find_rates( array( + if ( ! is_null( $tax_class ) ) { + // This will be per item shipping + $matched_tax_rates = self::find_shipping_rates( array( 'country' => $country, 'state' => $state, - 'city' => $city, 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $tax_class, ) ); - if ( $rates ) - foreach ( $rates as $key => $rate ) - if ( isset( $rate['shipping'] ) && $rate['shipping'] == 'yes' ) - $matched_tax_rates[ $key ] = $rate; - } + } else { - return $matched_tax_rates; + // This will be per order shipping - loop through the order and find the highest tax class rate + $cart_tax_classes = WC()->cart->get_cart_item_tax_classes(); - } else { - - // This will be per order shipping - loop through the order and find the highest tax class rate - $found_tax_classes = array(); - $matched_tax_rates = array(); - $rates = false; - - // Loop cart and find the highest tax band - if ( sizeof( WC()->cart->get_cart() ) > 0 ) - foreach ( WC()->cart->get_cart() as $item ) - $found_tax_classes[] = $item['data']->get_tax_class(); - - $found_tax_classes = array_unique( $found_tax_classes ); - - // If multiple classes are found, use highest - if ( sizeof( $found_tax_classes ) > 1 ) { - - if ( in_array( '', $found_tax_classes ) ) { - $rates = self::find_rates( array( - 'country' => $country, - 'state' => $state, - 'city' => $city, - 'postcode' => $postcode, - ) ); - } else { - $tax_classes = array_filter( array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ) ); + // If multiple classes are found, use the first one found unless a standard rate item is found. This will be the first listed in the 'additonal tax class' section. + if ( sizeof( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes ) ) { + $tax_classes = self::get_tax_class_slugs(); foreach ( $tax_classes as $tax_class ) { - if ( in_array( $tax_class, $found_tax_classes ) ) { - $rates = self::find_rates( array( + if ( in_array( $tax_class, $cart_tax_classes ) ) { + $matched_tax_rates = self::find_shipping_rates( array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, - 'tax_class' => $tax_class + 'tax_class' => $tax_class, ) ); break; } } + + // If a single tax class is found, use it + } elseif ( sizeof( $cart_tax_classes ) == 1 ) { + $matched_tax_rates = self::find_shipping_rates( array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $cart_tax_classes[0], + ) ); } - - // If a single tax class is found, use it - } elseif ( sizeof( $found_tax_classes ) == 1 ) { - - $rates = self::find_rates( array( - 'country' => $country, - 'state' => $state, - 'postcode' => $postcode, - 'city' => $city, - 'tax_class' => $found_tax_classes[0] - ) ); - } - // If no class rate are found, use standard rates - if ( ! $rates ) - $rates = self::find_rates( array( + // Get standard rate if no taxes were found + if ( ! sizeof( $matched_tax_rates ) ) { + $matched_tax_rates = self::find_shipping_rates( array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, ) ); - - if ( $rates ) - foreach ( $rates as $key => $rate ) - if ( isset( $rate['shipping'] ) && $rate['shipping'] == 'yes' ) - $matched_tax_rates[ $key ] = $rate; - - return $matched_tax_rates; + } } - return array(); // return false + return $matched_tax_rates; } /** * Return true/false depending on if a rate is a compound rate. * - * @param int key + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format * @return bool */ - public static function is_compound( $key ) { + public static function is_compound( $key_or_rate ) { global $wpdb; - return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ) ? true : false; + + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $compound = $key_or_rate->tax_rate_compound; + } else { + $key = $key_or_rate; + $compound = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ) ? true : false; + } + + return (bool) apply_filters( 'woocommerce_rate_compound', $compound, $key ); } /** * Return a given rates label. * - * @param int key + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format * @return string */ - public static function get_rate_label( $key ) { + public static function get_rate_label( $key_or_rate ) { global $wpdb; - $rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $rate_name = $key_or_rate->tax_rate_name; + } else { + $key = $key_or_rate; + $rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } - if ( ! $rate_name ) + if ( ! $rate_name ) { $rate_name = WC()->countries->tax_or_vat(); + } return apply_filters( 'woocommerce_rate_label', $rate_name, $key ); } /** - * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1 + * Return a given rates percent. * - * @access public - * @param mixed $key - * @return string + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format + * @return string */ - public static function get_rate_code( $key ) { + public static function get_rate_percent( $key_or_rate ) { global $wpdb; - $rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); - - if ( ! $rate ) { - return ''; + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $tax_rate = $key_or_rate->tax_rate; + } else { + $key = $key_or_rate; + $tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); } - $code = array(); + return apply_filters( 'woocommerce_rate_percent', floatval( $tax_rate ) . '%', $key ); + } - $code[] = $rate->tax_rate_country; - $code[] = $rate->tax_rate_state; - $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; - $code[] = absint( $rate->tax_rate_priority ); + /** + * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1. + * + * @access public + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format + * @return string + */ + public static function get_rate_code( $key_or_rate ) { + global $wpdb; - return apply_filters( 'woocommerce_rate_code', strtoupper( implode( '-', array_filter( $code ) ) ), $key ); + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $rate = $key_or_rate; + } else { + $key = $key_or_rate; + $rate = $wpdb->get_row( $wpdb->prepare( "SELECT tax_rate_country, tax_rate_state, tax_rate_name, tax_rate_priority FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + $code_string = ''; + + if ( null !== $rate ) { + $code = array(); + $code[] = $rate->tax_rate_country; + $code[] = $rate->tax_rate_state; + $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; + $code[] = absint( $rate->tax_rate_priority ); + $code_string = strtoupper( implode( '-', array_filter( $code ) ) ); + } + + return apply_filters( 'woocommerce_rate_code', $code_string, $key ); } /** @@ -566,5 +706,320 @@ class WC_Tax { public static function get_tax_total( $taxes ) { return array_sum( array_map( array( __CLASS__, 'round' ), $taxes ) ); } + + /** + * Get store tax classes. + * @return array Array of class names ("Reduced rate", "Zero rate", etc). + */ + public static function get_tax_classes() { + return array_filter( array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ) ); + } + + /** + * Get store tax classes as slugs. + * + * @since 3.0.0 + * @return array Array of class slugs ("reduced-rate", "zero-rate", etc). + */ + public static function get_tax_class_slugs() { + return array_map( 'sanitize_title', self::get_tax_classes() ); + } + + /** + * format the city. + * @param string $city + * @return string + */ + private static function format_tax_rate_city( $city ) { + return strtoupper( trim( $city ) ); + } + + /** + * format the state. + * @param string $state + * @return string + */ + private static function format_tax_rate_state( $state ) { + $state = strtoupper( $state ); + return ( '*' === $state ) ? '' : $state; + } + + /** + * format the country. + * @param string $country + * @return string + */ + private static function format_tax_rate_country( $country ) { + $country = strtoupper( $country ); + return ( '*' === $country ) ? '' : $country; + } + + /** + * format the tax rate name. + * @param string $name + * @return string + */ + private static function format_tax_rate_name( $name ) { + return $name ? $name : __( 'Tax', 'woocommerce' ); + } + + /** + * format the rate. + * @param double $rate + * @return string + */ + private static function format_tax_rate( $rate ) { + return number_format( (double) $rate, 4, '.', '' ); + } + + /** + * format the priority. + * @param string $priority + * @return int + */ + private static function format_tax_rate_priority( $priority ) { + return absint( $priority ); + } + + /** + * format the class. + * @param string $class + * @return string + */ + public static function format_tax_rate_class( $class ) { + $class = sanitize_title( $class ); + $classes = self::get_tax_class_slugs(); + if ( ! in_array( $class, $classes ) ) { + $class = ''; + } + return ( 'standard' === $class ) ? '' : $class; + } + + /** + * Prepare and format tax rate for DB insertion. + * @param array $tax_rate + * @return array + */ + private static function prepare_tax_rate( $tax_rate ) { + foreach ( $tax_rate as $key => $value ) { + if ( method_exists( __CLASS__, 'format_' . $key ) ) { + $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value ); + } + } + return $tax_rate; + } + + /** + * Insert a new tax rate. + * + * Internal use only. + * + * @since 2.3.0 + * @access private + * + * @param array $tax_rate + * + * @return int tax rate id + */ + public static function _insert_tax_rate( $tax_rate ) { + global $wpdb; + + $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) ); + + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + + do_action( 'woocommerce_tax_rate_added', $wpdb->insert_id, $tax_rate ); + + return $wpdb->insert_id; + } + + /** + * Get tax rate. + * + * Internal use only. + * + * @since 2.5.0 + * @access private + * + * @param int $tax_rate_id + * @param string $output_type + * + * @return array|object + */ + public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) { + global $wpdb; + + return $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE tax_rate_id = %d + ", $tax_rate_id ), $output_type ); + } + + /** + * Update a tax rate. + * + * Internal use only. + * + * @since 2.3.0 + * @access private + * + * @param int $tax_rate_id + * @param array $tax_rate + */ + public static function _update_tax_rate( $tax_rate_id, $tax_rate ) { + global $wpdb; + + $tax_rate_id = absint( $tax_rate_id ); + + $wpdb->update( + $wpdb->prefix . "woocommerce_tax_rates", + self::prepare_tax_rate( $tax_rate ), + array( + 'tax_rate_id' => $tax_rate_id, + ) + ); + + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + + do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate ); + } + + /** + * Delete a tax rate from the database. + * + * Internal use only. + * + * @since 2.3.0 + * @access private + * + * @param int $tax_rate_id + */ + public static function _delete_tax_rate( $tax_rate_id ) { + global $wpdb; + + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) ); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) ); + + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + + do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id ); + } + + /** + * Update postcodes for a tax rate in the DB. + * + * Internal use only. + * + * @since 2.3.0 + * @access private + * + * @param int $tax_rate_id + * @param string $postcodes String of postcodes separated by ; characters + * @return string + */ + public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) { + if ( ! is_array( $postcodes ) ) { + $postcodes = explode( ';', $postcodes ); + } + // No normalization - postcodes are matched against both normal and formatted versions to support wildcards. + foreach ( $postcodes as $key => $postcode ) { + $postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) ); + } + self::_update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' ); + } + + /** + * Update cities for a tax rate in the DB. + * + * Internal use only. + * + * @since 2.3.0 + * @access private + * + * @param int $tax_rate_id + * @param string $cities + * @return string + */ + public static function _update_tax_rate_cities( $tax_rate_id, $cities ) { + if ( ! is_array( $cities ) ) { + $cities = explode( ';', $cities ); + } + $cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) ); + + self::_update_tax_rate_locations( $tax_rate_id, $cities, 'city' ); + } + + /** + * Updates locations (postcode and city). + * + * Internal use only. + * + * @since 2.3.0 + * @access private + * + * @param int $tax_rate_id + * @param array $values + * @param string $type + * + * @return string + */ + private static function _update_tax_rate_locations( $tax_rate_id, $values, $type ) { + global $wpdb; + + $tax_rate_id = absint( $tax_rate_id ); + + $wpdb->query( + $wpdb->prepare( " + DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s; + ", $tax_rate_id, $type + ) + ); + + if ( sizeof( $values ) > 0 ) { + $sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )"; + + $wpdb->query( " + INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql; + " ); + } + + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + } + + /** + * Used by admin settings page. + * + * @param string $tax_class + * + * @return array|null|object + */ + public static function get_rates_for_tax_class( $tax_class ) { + global $wpdb; + + // Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries. + $rates = self::sort_rates( $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", sanitize_title( $tax_class ) ) ) ); + $locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" ); + + if ( ! empty( $rates ) ) { + // Set the rates keys equal to their ids. + $rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates ); + } + + // Drop the locations into the rates array. + foreach ( $locations as $location ) { + // Don't set them for unexistent rates. + if ( ! isset( $rates[ $location->tax_rate_id ] ) ) { + continue; + } + // If the rate exists, initialize the array before appending to it. + if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) { + $rates[ $location->tax_rate_id ]->{$location->location_type} = array(); + } + $rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code; + } + + return $rates; + } } -WC_Tax::init(); \ No newline at end of file +WC_Tax::init(); diff --git a/includes/class-wc-template-loader.php b/includes/class-wc-template-loader.php index 01e2bd6a108..334dd40a093 100644 --- a/includes/class-wc-template-loader.php +++ b/includes/class-wc-template-loader.php @@ -1,4 +1,9 @@ template_path() . $file; - - } elseif ( is_product_taxonomy() ) { - - $term = get_queried_object(); - - if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { - $file = 'taxonomy-' . $term->taxonomy . '.php'; - } else { - $file = 'archive-product.php'; - } - - $find[] = 'taxonomy-' . $term->taxonomy . '-' . $term->slug . '.php'; - $find[] = WC()->template_path() . 'taxonomy-' . $term->taxonomy . '-' . $term->slug . '.php'; - $find[] = 'taxonomy-' . $term->taxonomy . '.php'; - $find[] = WC()->template_path() . 'taxonomy-' . $term->taxonomy . '.php'; - $find[] = $file; - $find[] = WC()->template_path() . $file; - - } elseif ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) { - - $file = 'archive-product.php'; - $find[] = $file; - $find[] = WC()->template_path() . $file; - + if ( is_embed() ) { + return $template; } - if ( $file ) { - $template = locate_template( array_unique( $find ) ); - $status_options = get_option( 'woocommerce_status_options', array() ); - if ( ! $template || ( ! empty( $status_options['template_debug_mode'] ) && current_user_can( 'manage_options' ) ) ) { - $template = WC()->plugin_path() . '/templates/' . $file; + if ( $default_file = self::get_template_loader_default_file() ) { + /** + * Filter hook to choose which files to find before WooCommerce does it's own logic. + * + * @since 3.0.0 + * @var array + */ + $search_files = self::get_template_loader_files( $default_file ); + $template = locate_template( $search_files ); + + if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) { + $template = WC()->plugin_path() . '/templates/' . $default_file; } } @@ -80,25 +62,83 @@ class WC_Template_Loader { } /** - * comments_template_loader function. + * Get the default filename for a template. + * + * @since 3.0.0 + * @return string + */ + private static function get_template_loader_default_file() { + if ( is_singular( 'product' ) ) { + $default_file = 'single-product.php'; + } elseif ( is_product_taxonomy() ) { + $term = get_queried_object(); + + if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { + $default_file = 'taxonomy-' . $term->taxonomy . '.php'; + } else { + $default_file = 'archive-product.php'; + } + } elseif ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) { + $default_file = 'archive-product.php'; + } else { + $default_file = ''; + } + return $default_file; + } + + /** + * Get an array of filenames to search for a given template. + * + * @since 3.0.0 + * @param string $default_file The default file name. + * @return string[] + */ + private static function get_template_loader_files( $default_file ) { + $search_files = apply_filters( 'woocommerce_template_loader_files', array(), $default_file ); + $search_files[] = 'woocommerce.php'; + + if ( is_product_taxonomy() ) { + $term = get_queried_object(); + $search_files[] = 'taxonomy-' . $term->taxonomy . '-' . $term->slug . '.php'; + $search_files[] = WC()->template_path() . 'taxonomy-' . $term->taxonomy . '-' . $term->slug . '.php'; + $search_files[] = 'taxonomy-' . $term->taxonomy . '.php'; + $search_files[] = WC()->template_path() . 'taxonomy-' . $term->taxonomy . '.php'; + } + + $search_files[] = $default_file; + $search_files[] = WC()->template_path() . $default_file; + + return array_unique( $search_files ); + } + + /** + * Load comments template. * * @param mixed $template * @return string */ public static function comments_template_loader( $template ) { - if ( get_post_type() !== 'product' ) + if ( get_post_type() !== 'product' ) { return $template; + } - if ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . 'single-product-reviews.php' )) - return get_stylesheet_directory() . '/' . WC()->template_path() . 'single-product-reviews.php'; - elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . 'single-product-reviews.php' )) - return get_template_directory() . '/' . WC()->template_path() . 'single-product-reviews.php'; - elseif ( file_exists( get_stylesheet_directory() . '/' . 'single-product-reviews.php' )) - return get_stylesheet_directory() . '/' . 'single-product-reviews.php'; - elseif ( file_exists( get_template_directory() . '/' . 'single-product-reviews.php' )) - return get_template_directory() . '/' . 'single-product-reviews.php'; - else - return WC()->plugin_path() . '/templates/single-product-reviews.php'; + $check_dirs = array( + trailingslashit( get_stylesheet_directory() ) . WC()->template_path(), + trailingslashit( get_template_directory() ) . WC()->template_path(), + trailingslashit( get_stylesheet_directory() ), + trailingslashit( get_template_directory() ), + trailingslashit( WC()->plugin_path() ) . 'templates/', + ); + + if ( WC_TEMPLATE_DEBUG_MODE ) { + $check_dirs = array( array_pop( $check_dirs ) ); + } + + foreach ( $check_dirs as $dir ) { + if ( file_exists( trailingslashit( $dir ) . 'single-product-reviews.php' ) ) { + return trailingslashit( $dir ) . 'single-product-reviews.php'; + } + } } } diff --git a/includes/class-wc-tracker.php b/includes/class-wc-tracker.php new file mode 100644 index 00000000000..9c784c42fa3 --- /dev/null +++ b/includes/class-wc-tracker.php @@ -0,0 +1,401 @@ + apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) { + return; + } + } else { + // Make sure there is at least a 1 hour delay between override sends, we don't want duplicate calls due to double clicking links. + $last_send = self::get_last_send_time(); + if ( $last_send && $last_send > strtotime( '-1 hours' ) ) { + return; + } + } + + // Update time first before sending to ensure it is set + update_option( 'woocommerce_tracker_last_send', time() ); + + $params = self::get_tracking_data(); + wp_safe_remote_post( self::$api_url, array( + 'method' => 'POST', + 'timeout' => 45, + 'redirection' => 5, + 'httpversion' => '1.0', + 'blocking' => false, + 'headers' => array( 'user-agent' => 'WooCommerceTracker/' . md5( esc_url( home_url( '/' ) ) ) . ';' ), + 'body' => json_encode( $params ), + 'cookies' => array(), + ) + ); + } + + /** + * Get the last time tracking data was sent. + * @return int|bool + */ + private static function get_last_send_time() { + return apply_filters( 'woocommerce_tracker_last_send_time', get_option( 'woocommerce_tracker_last_send', false ) ); + } + + /** + * Get all the tracking data. + * @return array + */ + private static function get_tracking_data() { + $data = array(); + + // General site info + $data['url'] = home_url(); + $data['email'] = apply_filters( 'woocommerce_tracker_admin_email', get_option( 'admin_email' ) ); + $data['theme'] = self::get_theme_info(); + + // WordPress Info + $data['wp'] = self::get_wordpress_info(); + + // Server Info + $data['server'] = self::get_server_info(); + + // Plugin info + $all_plugins = self::get_all_plugins(); + $data['active_plugins'] = $all_plugins['active_plugins']; + $data['inactive_plugins'] = $all_plugins['inactive_plugins']; + + // Jetpack & WooCommerce Connect + $data['jetpack_version'] = defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : 'none'; + $data['jetpack_connected'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no'; + $data['jetpack_is_staging'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_staging_site' ) && Jetpack::is_staging_site() ) ? 'yes' : 'no'; + $data['connect_installed'] = class_exists( 'WC_Connect_Loader' ) ? 'yes' : 'no'; + $data['connect_active'] = ( class_exists( 'WC_Connect_Loader' ) && wp_next_scheduled( 'wc_connect_fetch_service_schemas' ) ) ? 'yes' : 'no'; + + // Store count info + $data['users'] = self::get_user_counts(); + $data['products'] = self::get_product_counts(); + $data['orders'] = self::get_order_counts(); + + // Payment gateway info + $data['gateways'] = self::get_active_payment_gateways(); + + // Shipping method info + $data['shipping_methods'] = self::get_active_shipping_methods(); + + // Get all WooCommerce options info + $data['settings'] = self::get_all_woocommerce_options_values(); + + // Template overrides + $data['template_overrides'] = self::get_all_template_overrides(); + + // Template overrides + $data['admin_user_agents'] = self::get_admin_user_agents(); + + return apply_filters( 'woocommerce_tracker_data', $data ); + } + + /** + * Get the current theme info, theme name and version. + * @return array + */ + public static function get_theme_info() { + $theme_data = wp_get_theme(); + $theme_child_theme = is_child_theme() ? 'Yes' : 'No'; + $theme_wc_support = ( ! current_theme_supports( 'woocommerce' ) && ! in_array( $theme_data->template, wc_get_core_supported_themes() ) ) ? 'No' : 'Yes'; + + return array( 'name' => $theme_data->Name, 'version' => $theme_data->Version, 'child_theme' => $theme_child_theme, 'wc_support' => $theme_wc_support ); + } + + /** + * Get WordPress related data. + * @return array + */ + private static function get_wordpress_info() { + $wp_data = array(); + + $memory = wc_let_to_num( WP_MEMORY_LIMIT ); + + if ( function_exists( 'memory_get_usage' ) ) { + $system_memory = wc_let_to_num( @ini_get( 'memory_limit' ) ); + $memory = max( $memory, $system_memory ); + } + + $wp_data['memory_limit'] = size_format( $memory ); + $wp_data['debug_mode'] = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No'; + $wp_data['locale'] = get_locale(); + $wp_data['version'] = get_bloginfo( 'version' ); + $wp_data['multisite'] = is_multisite() ? 'Yes' : 'No'; + + return $wp_data; + } + + /** + * Get server related info. + * @return array + */ + private static function get_server_info() { + $server_data = array(); + + if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) { + $server_data['software'] = $_SERVER['SERVER_SOFTWARE']; + } + + if ( function_exists( 'phpversion' ) ) { + $server_data['php_version'] = phpversion(); + } + + if ( function_exists( 'ini_get' ) ) { + $server_data['php_post_max_size'] = size_format( wc_let_to_num( ini_get( 'post_max_size' ) ) ); + $server_data['php_time_limt'] = ini_get( 'max_execution_time' ); + $server_data['php_max_input_vars'] = ini_get( 'max_input_vars' ); + $server_data['php_suhosin'] = extension_loaded( 'suhosin' ) ? 'Yes' : 'No'; + } + + global $wpdb; + $server_data['mysql_version'] = $wpdb->db_version(); + + $server_data['php_max_upload_size'] = size_format( wp_max_upload_size() ); + $server_data['php_default_timezone'] = date_default_timezone_get(); + $server_data['php_soap'] = class_exists( 'SoapClient' ) ? 'Yes' : 'No'; + $server_data['php_fsockopen'] = function_exists( 'fsockopen' ) ? 'Yes' : 'No'; + $server_data['php_curl'] = function_exists( 'curl_init' ) ? 'Yes' : 'No'; + + return $server_data; + } + + /** + * Get all plugins grouped into activated or not. + * @return array + */ + private static function get_all_plugins() { + // Ensure get_plugins function is loaded + if ( ! function_exists( 'get_plugins' ) ) { + include ABSPATH . '/wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + $active_plugins_keys = get_option( 'active_plugins', array() ); + $active_plugins = array(); + + foreach ( $plugins as $k => $v ) { + // Take care of formatting the data how we want it. + $formatted = array(); + $formatted['name'] = strip_tags( $v['Name'] ); + if ( isset( $v['Version'] ) ) { + $formatted['version'] = strip_tags( $v['Version'] ); + } + if ( isset( $v['Author'] ) ) { + $formatted['author'] = strip_tags( $v['Author'] ); + } + if ( isset( $v['Network'] ) ) { + $formatted['network'] = strip_tags( $v['Network'] ); + } + if ( isset( $v['PluginURI'] ) ) { + $formatted['plugin_uri'] = strip_tags( $v['PluginURI'] ); + } + if ( in_array( $k, $active_plugins_keys ) ) { + // Remove active plugins from list so we can show active and inactive separately + unset( $plugins[ $k ] ); + $active_plugins[ $k ] = $formatted; + } else { + $plugins[ $k ] = $formatted; + } + } + + return array( 'active_plugins' => $active_plugins, 'inactive_plugins' => $plugins ); + } + + /** + * Get user totals based on user role. + * @return array + */ + private static function get_user_counts() { + $user_count = array(); + $user_count_data = count_users(); + $user_count['total'] = $user_count_data['total_users']; + + // Get user count based on user role + foreach ( $user_count_data['avail_roles'] as $role => $count ) { + $user_count[ $role ] = $count; + } + + return $user_count; + } + + /** + * Get product totals based on product type. + * @return array + */ + private static function get_product_counts() { + $product_count = array(); + $product_count_data = wp_count_posts( 'product' ); + $product_count['total'] = $product_count_data->publish; + + $product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); + foreach ( $product_statuses as $product_status ) { + $product_count[ $product_status->name ] = $product_status->count; + } + + return $product_count; + } + + /** + * Get order counts based on order status. + * @return array + */ + private static function get_order_counts() { + $order_count = array(); + $order_count_data = wp_count_posts( 'shop_order' ); + + foreach ( wc_get_order_statuses() as $status_slug => $status_name ) { + $order_count[ $status_slug ] = $order_count_data->{ $status_slug }; + } + + return $order_count; + } + + /** + * Get a list of all active payment gateways. + * @return array + */ + private static function get_active_payment_gateways() { + $active_gateways = array(); + $gateways = WC()->payment_gateways->payment_gateways(); + foreach ( $gateways as $id => $gateway ) { + if ( isset( $gateway->enabled ) && 'yes' === $gateway->enabled ) { + $active_gateways[ $id ] = array( 'title' => $gateway->title, 'supports' => $gateway->supports ); + } + } + + return $active_gateways; + } + + /** + * Get a list of all active shipping methods. + * @return array + */ + private static function get_active_shipping_methods() { + $active_methods = array(); + $shipping_methods = WC()->shipping->get_shipping_methods(); + foreach ( $shipping_methods as $id => $shipping_method ) { + if ( isset( $shipping_method->enabled ) && 'yes' === $shipping_method->enabled ) { + $active_methods[ $id ] = array( 'title' => $shipping_method->title, 'tax_status' => $shipping_method->tax_status ); + } + } + + return $active_methods; + } + + /** + * Get all options starting with woocommerce_ prefix. + * @return array + */ + private static function get_all_woocommerce_options_values() { + return array( + 'version' => WC()->version, + 'currency' => get_woocommerce_currency(), + 'base_location' => WC()->countries->get_base_country(), + 'selling_locations' => WC()->countries->get_allowed_countries(), + 'api_enabled' => get_option( 'woocommerce_api_enabled' ), + 'weight_unit' => get_option( 'woocommerce_weight_unit' ), + 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), + 'download_method' => get_option( 'woocommerce_file_download_method' ), + 'download_require_login' => get_option( 'woocommerce_downloads_require_login' ), + 'calc_taxes' => get_option( 'woocommerce_calc_taxes' ), + 'coupons_enabled' => get_option( 'woocommerce_enable_coupons' ), + 'guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), + 'secure_checkout' => get_option( 'woocommerce_force_ssl_checkout' ), + 'enable_signup_and_login_from_checkout' => get_option( 'woocommerce_enable_signup_and_login_from_checkout' ), + 'enable_myaccount_registration' => get_option( 'woocommerce_enable_myaccount_registration' ), + 'registration_generate_username' => get_option( 'woocommerce_registration_generate_username' ), + 'registration_generate_password' => get_option( 'woocommerce_registration_generate_password' ), + ); + } + + /** + * Look for any template override and return filenames. + * @return array + */ + private static function get_all_template_overrides() { + $override_data = array(); + $template_paths = apply_filters( 'woocommerce_template_overrides_scan_paths', array( 'WooCommerce' => WC()->plugin_path() . '/templates/' ) ); + $scanned_files = array(); + + require_once( WC()->plugin_path() . '/includes/admin/class-wc-admin-status.php' ); + + foreach ( $template_paths as $plugin_name => $template_path ) { + $scanned_files[ $plugin_name ] = WC_Admin_Status::scan_template_files( $template_path ); + } + + foreach ( $scanned_files as $plugin_name => $files ) { + foreach ( $files as $file ) { + if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . $file; + } elseif ( file_exists( get_stylesheet_directory() . '/woocommerce/' . $file ) ) { + $theme_file = get_stylesheet_directory() . '/woocommerce/' . $file; + } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { + $theme_file = get_template_directory() . '/' . $file; + } elseif ( file_exists( get_template_directory() . '/woocommerce/' . $file ) ) { + $theme_file = get_template_directory() . '/woocommerce/' . $file; + } else { + $theme_file = false; + } + + if ( false !== $theme_file ) { + $override_data[] = basename( $theme_file ); + } + } + } + return $override_data; + } + + /** + * When an admin user logs in, there user agent is tracked in user meta and collected here. + * @return array + */ + private static function get_admin_user_agents() { + return array_filter( (array) get_option( 'woocommerce_tracker_ua', array() ) ); + } +} + +WC_Tracker::init(); diff --git a/includes/class-wc-validation.php b/includes/class-wc-validation.php index f3244428d6f..8e5bd0ccba1 100644 --- a/includes/class-wc-validation.php +++ b/includes/class-wc-validation.php @@ -1,19 +1,24 @@ 0 ) + if ( 0 < strlen( trim( preg_replace( '/[\s\#0-9_\-\+\/\(\)]/', '', $phone ) ) ) ) { return false; - - return true; - } - - /** - * Checks for a valid postcode - * - * @param string postcode - * @param string country - * @return bool - */ - public static function is_postcode( $postcode, $country ) { - if ( strlen( trim( preg_replace( '/[\s\-A-Za-z0-9]/', '', $postcode ) ) ) > 0 ) - return false; - - switch ( $country ) { - case "GB" : - return self::is_GB_postcode( $postcode ); - case "US" : - if ( preg_match( "/^([0-9]{5})(-[0-9]{4})?$/i", $postcode ) ) - return true; - else - return false; - case "CH" : - if ( preg_match( "/^([0-9]{4})$/i", $postcode ) ) - return true; - else - return false; - case "BR" : - if ( preg_match( "/^([0-9]{5,5})([-])?([0-9]{3,3})$/", $postcode ) ) - return true; - else - return false; } return true; } /** - * is_GB_postcode function. + * Checks for a valid postcode. + * + * @param string $postcode Postcode to validate. + * @param string $country Country to validate the postcode for. + * @return bool + */ + public static function is_postcode( $postcode, $country ) { + if ( strlen( trim( preg_replace( '/[\s\-A-Za-z0-9]/', '', $postcode ) ) ) > 0 ) { + return false; + } + + switch ( $country ) { + case 'AT' : + $valid = (bool) preg_match( '/^([0-9]{4})$/', $postcode ); + break; + case 'BR' : + $valid = (bool) preg_match( '/^([0-9]{5})([-])?([0-9]{3})$/', $postcode ); + break; + case 'CH' : + $valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode ); + break; + case 'DE' : + $valid = (bool) preg_match( '/^([0]{1}[1-9]{1}|[1-9]{1}[0-9]{1})[0-9]{3}$/', $postcode ); + break; + case 'ES' : + case 'FR' : + $valid = (bool) preg_match( '/^([0-9]{5})$/i', $postcode ); + break; + case 'GB' : + $valid = self::is_GB_postcode( $postcode ); + break; + case 'JP' : + $valid = (bool) preg_match( '/^([0-9]{3})([-])([0-9]{4})$/', $postcode ); + break; + case 'PT' : + $valid = (bool) preg_match( '/^([0-9]{4})([-])([0-9]{3})$/', $postcode ); + break; + case 'US' : + $valid = (bool) preg_match( '/^([0-9]{5})(-[0-9]{4})?$/i', $postcode ); + break; + case 'CA' : + // CA Postal codes cannot contain D,F,I,O,Q,U and cannot start with W or Z. https://en.wikipedia.org/wiki/Postal_codes_in_Canada#Number_of_possible_postal_codes + $valid = (bool) preg_match( '/^([ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ])([\ ])?(\d[ABCEGHJKLMNPRSTVWXYZ]\d)$/i', $postcode ); + break; + case 'PL': + $valid = (bool) preg_match( '/^([0-9]{2})([-])([0-9]{3})$/', $postcode ); + break; + + default : + $valid = true; + break; + } + + return apply_filters( 'woocommerce_validate_postcode', $valid, $postcode, $country ); + } + + /** + * Check if is a GB postcode. * * @author John Gardner - * @access public - * @param mixed $toCheck A postcode + * @param string $to_check A postcode * @return bool */ - public static function is_GB_postcode( $toCheck ) { + public static function is_GB_postcode( $to_check ) { // Permitted letters depend upon their position in the postcode. - // http://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation - $alpha1 = "[abcdefghijklmnoprstuwyz]"; // Character 1 - $alpha2 = "[abcdefghklmnopqrstuvwxy]"; // Character 2 - $alpha3 = "[abcdefghjkpstuw]"; // Character 3 == ABCDEFGHJKPSTUW - $alpha4 = "[abehmnprvwxy]"; // Character 4 == ABEHMNPRVWXY - $alpha5 = "[abdefghjlnpqrstuwxyz]"; // Character 5 != CIKMOV + // https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation + $alpha1 = "[abcdefghijklmnoprstuwyz]"; // Character 1 + $alpha2 = "[abcdefghklmnopqrstuvwxy]"; // Character 2 + $alpha3 = "[abcdefghjkpstuw]"; // Character 3 == ABCDEFGHJKPSTUW + $alpha4 = "[abehmnprvwxy]"; // Character 4 == ABEHMNPRVWXY + $alpha5 = "[abdefghjlnpqrstuwxyz]"; // Character 5 != CIKMOV + + $pcexp = array(); // Expression for postcodes: AN NAA, ANN NAA, AAN NAA, and AANN NAA - $pcexp[0] = '/^('.$alpha1.'{1}'.$alpha2.'{0,1}[0-9]{1,2})([0-9]{1}'.$alpha5.'{2})$/'; + $pcexp[0] = '/^(' . $alpha1 . '{1}' . $alpha2 . '{0,1}[0-9]{1,2})([0-9]{1}' . $alpha5 . '{2})$/'; // Expression for postcodes: ANA NAA - $pcexp[1] = '/^('.$alpha1.'{1}[0-9]{1}'.$alpha3.'{1})([0-9]{1}'.$alpha5.'{2})$/'; + $pcexp[1] = '/^(' . $alpha1 . '{1}[0-9]{1}' . $alpha3 . '{1})([0-9]{1}' . $alpha5 . '{2})$/'; // Expression for postcodes: AANA NAA - $pcexp[2] = '/^('.$alpha1.'{1}'.$alpha2.'[0-9]{1}'.$alpha4.')([0-9]{1}'.$alpha5.'{2})$/'; + $pcexp[2] = '/^(' . $alpha1 . '{1}' . $alpha2 . '[0-9]{1}' . $alpha4 . ')([0-9]{1}' . $alpha5 . '{2})$/'; // Exception for the special postcode GIR 0AA - $pcexp[3] = '/^(gir)(0aa)$/'; + $pcexp[3] = '/^(gir)(0aa)$/'; // Standard BFPO numbers $pcexp[4] = '/^(bfpo)([0-9]{1,4})$/'; @@ -104,23 +134,15 @@ class WC_Validation { $pcexp[5] = '/^(bfpo)(c\/o[0-9]{1,3})$/'; // Load up the string to check, converting into lowercase and removing spaces - $postcode = strtolower( $toCheck ); - $postcode = str_replace (' ', '', $postcode); + $postcode = strtolower( $to_check ); + $postcode = str_replace( ' ', '', $postcode ); // Assume we are not going to find a valid postcode $valid = false; // Check the string against the six types of postcodes foreach ( $pcexp as $regexp ) { - if ( preg_match( $regexp, $postcode, $matches ) ) { - - // Load new postcode back into the form element - $toCheck = strtoupper ($matches[1] . ' ' . $matches [2]); - - // Take account of the special BFPO c/o format - $toCheck = str_replace( 'C/O', 'c/o ', $toCheck ); - // Remember that we have found that the code is valid and break from loop $valid = true; break; @@ -131,24 +153,23 @@ class WC_Validation { } /** - * Format the postcode according to the country and length of the postcode + * Format the postcode according to the country and length of the postcode. * - * @param string postcode - * @param string country - * @return string formatted postcode + * @param string $postcode Postcode to format. + * @param string $country Country to format the postcode for. + * @return string Formatted postcode. */ public static function format_postcode( $postcode, $country ) { - wc_format_postcode( $postcode, $country ); + return wc_format_postcode( $postcode, $country ); } /** * format_phone function. * - * @access public - * @param mixed $tel + * @param mixed $tel Phone number to format. * @return string */ public static function format_phone( $tel ) { - wc_format_phone_number( $tel ); + return wc_format_phone_number( $tel ); } } diff --git a/includes/class-wc-webhook.php b/includes/class-wc-webhook.php new file mode 100644 index 00000000000..7722874cb0f --- /dev/null +++ b/includes/class-wc-webhook.php @@ -0,0 +1,937 @@ +id = $id; + $this->post_data = get_post( $id ); + } + + + /** + * Magic isset as a wrapper around metadata_exists(). + * + * @since 2.2 + * @param string $key + * @return bool true if $key isset, false otherwise + */ + public function __isset( $key ) { + if ( ! $this->id ) { + return false; + } + return metadata_exists( 'post', $this->id, '_' . $key ); + } + + + /** + * Magic get, wraps get_post_meta() for all keys except $status. + * + * @since 2.2 + * @param string $key + * @return mixed value + */ + public function __get( $key ) { + + if ( 'status' === $key ) { + $value = $this->get_status(); + } else { + $value = get_post_meta( $this->id, '_' . $key, true ); + } + + return $value; + } + + + /** + * Enqueue the hooks associated with the webhook. + * + * @since 2.2 + */ + public function enqueue() { + $hooks = $this->get_hooks(); + $url = $this->get_delivery_url(); + + if ( is_array( $hooks ) && ! empty( $url ) ) { + foreach ( $hooks as $hook ) { + add_action( $hook, array( $this, 'process' ) ); + } + } + } + + + /** + * Process the webhook for delivery by verifying that it should be delivered. + * and scheduling the delivery (in the background by default, or immediately). + * + * @since 2.2 + * @param mixed $arg the first argument provided from the associated hooks + */ + public function process( $arg ) { + + // verify that webhook should be processed for delivery + if ( ! $this->should_deliver( $arg ) ) { + return; + } + + // webhooks are processed in the background by default + // so as to avoid delays or failures in delivery from affecting the + // user who triggered it + if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $this, $arg ) ) { + + // deliver in background + wp_schedule_single_event( time(), 'woocommerce_deliver_webhook_async', array( $this->id, $arg ) ); + + } else { + + // deliver immediately + $this->deliver( $arg ); + } + } + + /** + * Helper to check if the webhook should be delivered, as some hooks. + * (like `wp_trash_post`) will fire for every post type, not just ours. + * + * @since 2.2 + * @param mixed $arg first hook argument + * @return bool true if webhook should be delivered, false otherwise + */ + private function should_deliver( $arg ) { + $should_deliver = true; + $current_action = current_action(); + + // only active webhooks can be delivered + if ( 'active' != $this->get_status() ) { + $should_deliver = false; + } elseif ( in_array( $current_action, array( 'delete_post', 'wp_trash_post', 'untrashed_post' ), true ) ) { + // Only deliver deleted/restored event for coupons, orders, and products. + if ( isset( $GLOBALS['post_type'] ) && ! in_array( $GLOBALS['post_type'], array( 'shop_coupon', 'shop_order', 'product' ) ) ) { + $should_deliver = false; + } + + // Check if is delivering for the correct resource. + if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) { + $should_deliver = false; + } + } elseif ( 'delete_user' == $current_action ) { + $user = get_userdata( absint( $arg ) ); + + // only deliver deleted customer event for users with customer role + if ( ! $user || ! in_array( 'customer', (array) $user->roles ) ) { + $should_deliver = false; + } + + // check if the custom order type has chosen to exclude order webhooks from triggering along with its own webhooks. + } elseif ( 'order' == $this->get_resource() && ! in_array( get_post_type( absint( $arg ) ), wc_get_order_types( 'order-webhooks' ) ) ) { + $should_deliver = false; + + } elseif ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) { + // the `woocommerce_process_shop_*` and `woocommerce_process_product_*` hooks + // fire for create and update of products and orders, so check the post + // creation date to determine the actual event + $resource = get_post( absint( $arg ) ); + + // Drafts don't have post_date_gmt so calculate it here + $gmt_date = get_gmt_from_date( $resource->post_date ); + + // a resource is considered created when the hook is executed within 10 seconds of the post creation date + $resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) ); + + if ( 'created' == $this->get_event() && ! $resource_created ) { + $should_deliver = false; + } elseif ( 'updated' == $this->get_event() && $resource_created ) { + $should_deliver = false; + } + } + + /* + * Let other plugins intercept deliver for some messages queue like rabbit/zeromq + */ + return apply_filters( 'woocommerce_webhook_should_deliver', $should_deliver, $this, $arg ); + } + + + /** + * Deliver the webhook payload using wp_safe_remote_request(). + * + * @since 2.2 + * @param mixed $arg First hook argument. + */ + public function deliver( $arg ) { + $payload = $this->build_payload( $arg ); + + // Setup request args. + $http_args = array( + 'method' => 'POST', + 'timeout' => MINUTE_IN_SECONDS, + 'redirection' => 0, + 'httpversion' => '1.0', + 'blocking' => true, + 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ), + 'body' => trim( json_encode( $payload ) ), + 'headers' => array( 'Content-Type' => 'application/json' ), + 'cookies' => array(), + ); + + $http_args = apply_filters( 'woocommerce_webhook_http_args', $http_args, $arg, $this->id ); + + // Add custom headers. + $http_args['headers']['X-WC-Webhook-Source'] = home_url( '/' ); // Since 2.6.0. + $http_args['headers']['X-WC-Webhook-Topic'] = $this->get_topic(); + $http_args['headers']['X-WC-Webhook-Resource'] = $this->get_resource(); + $http_args['headers']['X-WC-Webhook-Event'] = $this->get_event(); + $http_args['headers']['X-WC-Webhook-Signature'] = $this->generate_signature( $http_args['body'] ); + $http_args['headers']['X-WC-Webhook-ID'] = $this->id; + $http_args['headers']['X-WC-Webhook-Delivery-ID'] = $delivery_id = $this->get_new_delivery_id(); + + $start_time = microtime( true ); + + // Webhook away! + $response = wp_safe_remote_request( $this->get_delivery_url(), $http_args ); + + $duration = round( microtime( true ) - $start_time, 5 ); + + $this->log_delivery( $delivery_id, $http_args, $response, $duration ); + + do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->id ); + } + + /** + * Get Legacy API payload. + * + * @since 3.0.0 + * @param string $resource Resource type. + * @param int $resource_id Resource ID. + * @param string $event Event type. + * @return array + */ + private function get_legacy_api_payload( $resource, $resource_id, $event ) { + // Include & load API classes. + WC()->api->includes(); + WC()->api->register_resources( new WC_API_Server( '/' ) ); + + switch ( $resource ) { + case 'coupon' : + $payload = WC()->api->WC_API_Coupons->get_coupon( $resource_id ); + break; + + case 'customer' : + $payload = WC()->api->WC_API_Customers->get_customer( $resource_id ); + break; + + case 'order' : + $payload = WC()->api->WC_API_Orders->get_order( $resource_id, null, apply_filters( 'woocommerce_webhook_order_payload_filters', array() ) ); + break; + + case 'product' : + // Bulk and quick edit action hooks return a product object instead of an ID. + if ( 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) { + $resource_id = $resource_id->get_id(); + } + $payload = WC()->api->WC_API_Products->get_product( $resource_id ); + break; + + // Custom topics include the first hook argument. + case 'action' : + $payload = array( + 'action' => current( $this->get_hooks() ), + 'arg' => $resource_id, + ); + break; + + default : + $payload = array(); + break; + } + + return $payload; + } + + /** + * Get WP API integration payload. + * + * @since 3.0.0 + * @param string $resource Resource type. + * @param int $resource_id Resource ID. + * @param string $event Event type. + * @return array + */ + private function get_wp_api_payload( $resource, $resource_id, $event ) { + $version_suffix = 'wp_api_v1' === $this->get_api_version() ? '_V1' : ''; + + switch ( $resource ) { + case 'coupon' : + case 'customer' : + case 'order' : + case 'product' : + $class = 'WC_REST_' . ucfirst( $resource ) . 's' . $version_suffix . '_Controller'; + $request = new WP_REST_Request( 'GET' ); + $controller = new $class; + + // Bulk and quick edit action hooks return a product object instead of an ID. + if ( 'product' === $resource && 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) { + $resource_id = $resource_id->get_id(); + } + + $request->set_param( 'id', $resource_id ); + $result = $controller->get_item( $request ); + $payload = isset( $result->data ) ? $result->data : array(); + break; + + // Custom topics include the first hook argument. + case 'action' : + $payload = array( + 'action' => current( $this->get_hooks() ), + 'arg' => $resource_id, + ); + break; + + default : + $payload = array(); + break; + } + + return $payload; + } + + /** + * Build the payload data for the webhook. + * + * @since 2.2 + * @param mixed $resource_id first hook argument, typically the resource ID + * @return mixed payload data + */ + public function build_payload( $resource_id ) { + // build the payload with the same user context as the user who created + // the webhook -- this avoids permission errors as background processing + // runs with no user context + $current_user = get_current_user_id(); + wp_set_current_user( $this->get_user_id() ); + + $resource = $this->get_resource(); + $event = $this->get_event(); + + // If a resource has been deleted, just include the ID. + if ( 'deleted' == $event ) { + $payload = array( + 'id' => $resource_id, + ); + } else { + if ( in_array( $this->get_api_version(), array( 'wp_api_v1', 'wp_api_v2' ), true ) ) { + $payload = $this->get_wp_api_payload( $resource, $resource_id, $event ); + } else { + $payload = $this->get_legacy_api_payload( $resource, $resource_id, $event ); + } + } + + // Restore the current user. + wp_set_current_user( $current_user ); + + return apply_filters( 'woocommerce_webhook_payload', $payload, $resource, $resource_id, $this->id ); + } + + + /** + * Generate a base64-encoded HMAC-SHA256 signature of the payload body so the. + * recipient can verify the authenticity of the webhook. Note that the signature. + * is calculated after the body has already been encoded (JSON by default). + * + * @since 2.2 + * @param string $payload payload data to hash + * @return string hash + */ + public function generate_signature( $payload ) { + + $hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->id ); + + return base64_encode( hash_hmac( $hash_algo, $payload, $this->get_secret(), true ) ); + } + + + /** + * Create a new comment for log the delivery request/response and. + * return the ID for inclusion in the webhook request. + * + * @since 2.2 + * @return int delivery (comment) ID + */ + public function get_new_delivery_id() { + + $comment_data = apply_filters( 'woocommerce_new_webhook_delivery_data', array( + 'comment_author' => __( 'WooCommerce', 'woocommerce' ), + 'comment_author_email' => sanitize_email( sprintf( '%s@%s', strtolower( __( 'WooCommerce', 'woocommerce' ) ), isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', $_SERVER['HTTP_HOST'] ) : 'noreply.com' ) ), + 'comment_post_ID' => $this->id, + 'comment_agent' => 'WooCommerce Hookshot', + 'comment_type' => 'webhook_delivery', + 'comment_parent' => 0, + 'comment_approved' => 1, + ), $this->id ); + + $comment_id = wp_insert_comment( $comment_data ); + + return $comment_id; + } + + + /** + * Log the delivery request/response. + * + * @since 2.2 + * @param int $delivery_id previously created comment ID + * @param array $request request data + * @param array|WP_Error $response response data + * @param float $duration request duration + */ + public function log_delivery( $delivery_id, $request, $response, $duration ) { + + // save request data + add_comment_meta( $delivery_id, '_request_method', $request['method'] ); + add_comment_meta( $delivery_id, '_request_headers', array_merge( array( 'User-Agent' => $request['user-agent'] ), $request['headers'] ) ); + add_comment_meta( $delivery_id, '_request_body', wp_slash( $request['body'] ) ); + + // parse response + if ( is_wp_error( $response ) ) { + $response_code = $response->get_error_code(); + $response_message = $response->get_error_message(); + $response_headers = array(); + $response_body = ''; + + } else { + $response_code = wp_remote_retrieve_response_code( $response ); + $response_message = wp_remote_retrieve_response_message( $response ); + $response_headers = wp_remote_retrieve_headers( $response ); + $response_body = wp_remote_retrieve_body( $response ); + } + + // save response data + add_comment_meta( $delivery_id, '_response_code', $response_code ); + add_comment_meta( $delivery_id, '_response_message', $response_message ); + add_comment_meta( $delivery_id, '_response_headers', $response_headers ); + add_comment_meta( $delivery_id, '_response_body', $response_body ); + + // save duration + add_comment_meta( $delivery_id, '_duration', $duration ); + + // set a summary for quick display + $args = array( + 'comment_ID' => $delivery_id, + 'comment_content' => sprintf( 'HTTP %s %s: %s', $response_code, $response_message, $response_body ), + ); + + wp_update_comment( $args ); + + // track failures + if ( intval( $response_code ) >= 200 && intval( $response_code ) < 300 ) { + delete_post_meta( $this->id, '_failure_count' ); + } else { + $this->failed_delivery(); + } + + // keep the 25 most recent delivery logs + $log = wp_count_comments( $this->id ); + if ( $log->total_comments > apply_filters( 'woocommerce_max_webhook_delivery_logs', 25 ) ) { + global $wpdb; + + $comment_id = $wpdb->get_var( $wpdb->prepare( "SELECT comment_ID FROM {$wpdb->comments} WHERE comment_post_ID = %d ORDER BY comment_date_gmt ASC LIMIT 1", $this->id ) ); + + if ( $comment_id ) { + wp_delete_comment( $comment_id, true ); + } + } + } + + /** + * Track consecutive delivery failures and automatically disable the webhook. + * if more than 5 consecutive failures occur. A failure is defined as a. + * non-2xx response. + * + * @since 2.2 + */ + private function failed_delivery() { + + $failures = $this->get_failure_count(); + + if ( $failures > apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 ) ) { + + $this->update_status( 'disabled' ); + + } else { + + update_post_meta( $this->id, '_failure_count', ++$failures ); + } + } + + + /** + * Get the delivery logs for this webhook. + * + * @since 2.2 + * @return array + */ + public function get_delivery_logs() { + + $args = array( + 'post_id' => $this->id, + 'status' => 'approve', + 'type' => 'webhook_delivery', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_webhook_comments' ), 10, 1 ); + + $logs = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_webhook_comments' ), 10, 1 ); + + $delivery_logs = array(); + + foreach ( $logs as $log ) { + + $log = $this->get_delivery_log( $log->comment_ID ); + + $delivery_logs[] = ( ! empty( $log ) ? $log : array() ); + } + + return $delivery_logs; + } + + + /** + * Get the delivery log specified by the ID. The delivery log includes: + * + * + duration + * + summary + * + request method/url + * + request headers/body + * + response code/message/headers/body + * + * @since 2.2 + * @param int $delivery_id + * @return bool|array false if invalid delivery ID, array of log data otherwise + */ + public function get_delivery_log( $delivery_id ) { + + $log = get_comment( $delivery_id ); + + // valid comment and ensure delivery log belongs to this webhook + if ( is_null( $log ) || $log->comment_post_ID != $this->id ) { + return false; + } + + $delivery_log = array( + 'id' => intval( $delivery_id ), + 'duration' => get_comment_meta( $delivery_id, '_duration', true ), + 'summary' => $log->comment_content, + 'request_method' => get_comment_meta( $delivery_id, '_request_method', true ), + 'request_url' => $this->get_delivery_url(), + 'request_headers' => get_comment_meta( $delivery_id, '_request_headers', true ), + 'request_body' => get_comment_meta( $delivery_id, '_request_body', true ), + 'response_code' => get_comment_meta( $delivery_id, '_response_code', true ), + 'response_message' => get_comment_meta( $delivery_id, '_response_message', true ), + 'response_headers' => get_comment_meta( $delivery_id, '_response_headers', true ), + 'response_body' => get_comment_meta( $delivery_id, '_response_body', true ), + 'comment' => $log, + ); + + return apply_filters( 'woocommerce_webhook_delivery_log', $delivery_log, $delivery_id, $this->id ); + } + + /** + * Set the webhook topic and associated hooks. The topic resource & event. + * are also saved separately. + * + * @since 2.2 + * @param string $topic + */ + public function set_topic( $topic ) { + + $topic = strtolower( $topic ); + + list( $resource, $event ) = explode( '.', $topic ); + + update_post_meta( $this->id, '_topic', $topic ); + update_post_meta( $this->id, '_resource', $resource ); + update_post_meta( $this->id, '_event', $event ); + + // custom topics are mapped to a single hook + if ( 'action' === $resource ) { + + update_post_meta( $this->id, '_hooks', array( $event ) ); + + } else { + + // API topics have multiple hooks + update_post_meta( $this->id, '_hooks', $this->get_topic_hooks( $topic ) ); + } + } + + /** + * Get the associated hook names for a topic. + * + * @since 2.2 + * @param string $topic + * @return array hook names + */ + private function get_topic_hooks( $topic ) { + + $topic_hooks = array( + 'coupon.created' => array( + 'woocommerce_process_shop_coupon_meta', + 'woocommerce_api_create_coupon', + ), + 'coupon.updated' => array( + 'woocommerce_process_shop_coupon_meta', + 'woocommerce_api_edit_coupon', + ), + 'coupon.deleted' => array( + 'wp_trash_post', + ), + 'coupon.restored' => array( + 'untrashed_post', + ), + 'customer.created' => array( + 'user_register', + 'woocommerce_created_customer', + 'woocommerce_api_create_customer', + ), + 'customer.updated' => array( + 'profile_update', + 'woocommerce_api_edit_customer', + 'woocommerce_customer_save_address', + ), + 'customer.deleted' => array( + 'delete_user', + ), + 'order.created' => array( + 'woocommerce_checkout_order_processed', + 'woocommerce_process_shop_order_meta', + 'woocommerce_api_create_order', + ), + 'order.updated' => array( + 'woocommerce_process_shop_order_meta', + 'woocommerce_api_edit_order', + 'woocommerce_order_edit_status', + 'woocommerce_order_status_changed', + ), + 'order.deleted' => array( + 'wp_trash_post', + ), + 'order.restored' => array( + 'untrashed_post', + ), + 'product.created' => array( + 'woocommerce_process_product_meta', + 'woocommerce_api_create_product', + ), + 'product.updated' => array( + 'woocommerce_process_product_meta', + 'woocommerce_api_edit_product', + 'woocommerce_product_quick_edit_save', + 'woocommerce_product_bulk_edit_save', + ), + 'product.deleted' => array( + 'wp_trash_post', + ), + 'product.restored' => array( + 'untrashed_post', + ), + ); + + $topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this ); + + return isset( $topic_hooks[ $topic ] ) ? $topic_hooks[ $topic ] : array(); + } + + /** + * Send a test ping to the delivery URL, sent when the webhook is first created. + * + * @since 2.2 + * @return bool|WP_Error + */ + public function deliver_ping() { + $args = array( + 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ), + 'body' => "webhook_id={$this->id}", + ); + + $test = wp_safe_remote_post( $this->get_delivery_url(), $args ); + $response_code = wp_remote_retrieve_response_code( $test ); + + if ( is_wp_error( $test ) ) { + return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL cannot be reached: %s', 'woocommerce' ), $test->get_error_message() ) ); + } + + if ( 200 !== $response_code ) { + return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL returned response code: %s', 'woocommerce' ), absint( $response_code ) ) ); + } + + delete_post_meta( $this->id, '_webhook_pending_delivery' ); + + return true; + } + + /** + * Get the webhook status: + * + * + `active` - delivers payload. + * + `paused` - does not deliver payload, paused by admin. + * + `disabled` - does not delivery payload, paused automatically due to. + * consecutive failures. + * + * @since 2.2 + * @return string status + */ + public function get_status() { + + switch ( $this->get_post_data()->post_status ) { + + case 'publish': + $status = 'active'; + break; + + case 'draft': + $status = 'paused'; + break; + + case 'pending': + $status = 'disabled'; + break; + + default: + $status = 'paused'; + } + + return apply_filters( 'woocommerce_webhook_status', $status, $this->id ); + } + + /** + * Get the webhook i18n status. + * + * @return string + */ + public function get_i18n_status() { + $status = $this->get_status(); + $statuses = wc_get_webhook_statuses(); + + return isset( $statuses[ $status ] ) ? $statuses[ $status ] : $status; + } + + /** + * Update the webhook status, see get_status() for valid statuses. + * + * @since 2.2 + * @param $status + */ + public function update_status( $status ) { + global $wpdb; + + switch ( $status ) { + + case 'active' : + $post_status = 'publish'; + break; + + case 'paused' : + $post_status = 'draft'; + break; + + case 'disabled' : + $post_status = 'pending'; + break; + + default : + $post_status = 'draft'; + break; + } + + $wpdb->update( $wpdb->posts, array( 'post_status' => $post_status ), array( 'ID' => $this->id ) ); + clean_post_cache( $this->id ); + } + + /** + * Set the delivery URL. + * + * @since 2.2 + * @param string $url + */ + public function set_delivery_url( $url ) { + if ( update_post_meta( $this->id, '_delivery_url', esc_url_raw( $url, array( 'http', 'https' ) ) ) ) { + update_post_meta( $this->id, '_webhook_pending_delivery', true ); + } + } + + /** + * Get the delivery URL. + * + * @since 2.2 + * @return string + */ + public function get_delivery_url() { + + return apply_filters( 'woocommerce_webhook_delivery_url', $this->delivery_url, $this->id ); + } + + /** + * Set the secret used for generating the HMAC-SHA256 signature. + * + * @since 2.2 + * @param string $secret + */ + public function set_secret( $secret ) { + + update_post_meta( $this->id, '_secret', $secret ); + } + + /** + * Get the secret used for generating the HMAC-SHA256 signature. + * + * @since 2.2 + * @return string + */ + public function get_secret() { + return apply_filters( 'woocommerce_webhook_secret', $this->secret, $this->id ); + } + + /** + * Get the friendly name for the webhook. + * + * @since 2.2 + * @return string + */ + public function get_name() { + return apply_filters( 'woocommerce_webhook_name', $this->get_post_data()->post_title, $this->id ); + } + + /** + * Get the webhook topic, e.g. `order.created`. + * + * @since 2.2 + * @return string + */ + public function get_topic() { + return apply_filters( 'woocommerce_webhook_topic', $this->topic, $this->id ); + } + + /** + * Get the hook names for the webhook. + * + * @since 2.2 + * @return array hook names + */ + public function get_hooks() { + return apply_filters( 'woocommerce_webhook_hooks', $this->hooks, $this->id ); + } + + /** + * Get the resource for the webhook, e.g. `order`. + * + * @since 2.2 + * @return string + */ + public function get_resource() { + return apply_filters( 'woocommerce_webhook_resource', $this->resource, $this->id ); + } + + /** + * Get the event for the webhook, e.g. `created`. + * + * @since 2.2 + * @return string + */ + public function get_event() { + return apply_filters( 'woocommerce_webhook_event', $this->event, $this->id ); + } + + /** + * Get the failure count. + * + * @since 2.2 + * @return int + */ + public function get_failure_count() { + return intval( $this->failure_count ); + } + + /** + * Get the user ID for this webhook. + * + * @since 2.2 + * @return int|string user ID + */ + public function get_user_id() { + return $this->get_post_data()->post_author; + } + + /** + * Get the post data for the webhook. + * + * @since 2.2 + * @return null|WP_Post + */ + public function get_post_data() { + return $this->post_data; + } + + /** + * Set API version. + * + * @since 3.0.0 + * @param string $version REST API version. + */ + public function set_api_version( $version ) { + $versions = array( + 'wp_api_v2', + 'wp_api_v1', + 'legacy_v3', + ); + + if ( ! in_array( $version, $versions, true ) ) { + $version = 'wp_api_v2'; + } + + update_post_meta( $this->id, '_api_version', $version ); + } + + /** + * API version. + * + * @since 3.0.0 + * @return string + */ + public function get_api_version() { + return $this->api_version ? $this->api_version : 'legacy_v3'; + } +} diff --git a/includes/cli/class-wc-cli-rest-command.php b/includes/cli/class-wc-cli-rest-command.php new file mode 100644 index 00000000000..daad9e4ed73 --- /dev/null +++ b/includes/cli/class-wc-cli-rest-command.php @@ -0,0 +1,481 @@ + desc). + */ + private $supported_ids = array(); + + /** + * Sets up REST Command. + * + * @param string $name Name of endpoint object (comes from schema) + * @param string $route Path to route of this endpoint + * @param array $schema Schema object + */ + public function __construct( $name, $route, $schema ) { + $this->name = $name; + + preg_match_all( '#\([^\)]+\)#', $route, $matches ); + $first_match = $matches[0]; + $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; + $this->route = rtrim( $route ); + $this->schema = $schema; + + $this->resource_identifier = $resource_id; + if ( in_array( $name, $this->routes_with_parent_id ) ) { + $is_singular = substr( $this->route, - strlen( $resource_id ) ) === $resource_id; + if ( ! $is_singular ) { + $this->resource_identifier = $first_match[0]; + } + } + } + + /** + * Passes supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id. + * + * @param array $supported_ids + */ + public function set_supported_ids( $supported_ids = array() ) { + $this->supported_ids = $supported_ids; + } + + /** + * Peturns an ID of supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id. + * + * @return array + */ + public function get_supported_ids() { + return $this->supported_ids; + } + + /** + * Create a new item. + * + * @subcommand create + * + * @param array $args + * @param array $assoc_args + */ + public function create_item( $args, $assoc_args ) { + $assoc_args = self::decode_json( $assoc_args ); + list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { + WP_CLI::line( $body['id'] ); + } else { + WP_CLI::success( "Created {$this->name} {$body['id']}." ); + } + } + + /** + * Delete an existing item. + * + * @subcommand delete + * + * @param array $args + * @param array $assoc_args + */ + public function delete_item( $args, $assoc_args ) { + list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args ); + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { + WP_CLI::line( $body['id'] ); + } else { + if ( empty( $assoc_args['force'] ) ) { + WP_CLI::success( __( 'Trashed', 'woocommerce' ) . " {$this->name} {$body['id']}" ); + } else { + WP_CLI::success( __( 'Deleted', 'woocommerce' ) . " {$this->name} {$body['id']}." ); + } + } + } + + /** + * Get a single item. + * + * @subcommand get + * + * @param array $args + * @param array $assoc_args + */ + public function get_item( $args, $assoc_args ) { + $route = $this->get_filled_route( $args ); + list( $status, $body, $headers ) = $this->do_request( 'GET', $route, $assoc_args ); + + if ( ! empty( $assoc_args['fields'] ) ) { + $body = self::limit_item_to_fields( $body, $assoc_args['fields'] ); + } + + if ( 'headers' === $assoc_args['format'] ) { + echo json_encode( $headers ); + } elseif ( 'body' === $assoc_args['format'] ) { + echo json_encode( $body ); + } elseif ( 'envelope' === $assoc_args['format'] ) { + echo json_encode( array( + 'body' => $body, + 'headers' => $headers, + 'status' => $status, + ) ); + } else { + $formatter = $this->get_formatter( $assoc_args ); + $formatter->display_item( $body ); + } + } + + /** + * List all items. + * + * @subcommand list + * + * @param array $args + * @param array $assoc_args + */ + public function list_items( $args, $assoc_args ) { + if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { + $method = 'HEAD'; + } else { + $method = 'GET'; + } + + list( $status, $body, $headers ) = $this->do_request( $method, $this->get_filled_route( $args ), $assoc_args ); + if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { + $items = array_column( $body, 'id' ); + } else { + $items = $body; + } + + if ( ! empty( $assoc_args['fields'] ) ) { + foreach ( $items as $key => $item ) { + $items[ $key ] = self::limit_item_to_fields( $item, $assoc_args['fields'] ); + } + } + + if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { + echo (int) $headers['X-WP-Total']; + } elseif ( 'headers' === $assoc_args['format'] ) { + echo json_encode( $headers ); + } elseif ( 'body' === $assoc_args['format'] ) { + echo json_encode( $body ); + } elseif ( 'envelope' === $assoc_args['format'] ) { + echo json_encode( array( + 'body' => $body, + 'headers' => $headers, + 'status' => $status, + 'api_url' => $this->api_url, + ) ); + } else { + $formatter = $this->get_formatter( $assoc_args ); + $formatter->display_items( $items ); + } + } + + /** + * Update an existing item. + * + * @subcommand update + * + * @param array $args + * @param array $assoc_args + */ + public function update_item( $args, $assoc_args ) { + $assoc_args = self::decode_json( $assoc_args ); + list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { + WP_CLI::line( $body['id'] ); + } else { + WP_CLI::success( __( 'Updated', 'woocommerce' ) . " {$this->name} {$body['id']}." ); + } + } + + /** + * Do a REST Request + * + * @param string $method + * @param string $route + * @param array $assoc_args + * + * @return array + */ + private function do_request( $method, $route, $assoc_args ) { + if ( ! defined( 'REST_REQUEST' ) ) { + define( 'REST_REQUEST', true ); + } + $request = new WP_REST_Request( $method, $route ); + if ( in_array( $method, array( 'POST', 'PUT' ) ) ) { + $request->set_body_params( $assoc_args ); + } else { + foreach ( $assoc_args as $key => $value ) { + $request->set_param( $key, $value ); + } + } + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array(); + } + $response = rest_do_request( $request ); + if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + $performed_queries = array(); + foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) { + if ( in_array( $key, $original_queries ) ) { + continue; + } + $performed_queries[] = $query; + } + usort( $performed_queries, function( $a, $b ) { + if ( $a[1] === $b[1] ) { + return 0; + } + return ( $a[1] > $b[1] ) ? -1 : 1; + }); + + $query_count = count( $performed_queries ); + $query_total_time = 0; + foreach ( $performed_queries as $query ) { + $query_total_time += $query[1]; + } + $slow_query_message = ''; + if ( $performed_queries && 'wc' === WP_CLI::get_config( 'debug' ) ) { + $slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL; + foreach ( $performed_queries as $i => $query ) { + $i++; + $bits = explode( ', ', $query[2] ); + $backtrace = implode( ', ', array_slice( $bits, 13 ) ); + $seconds = round( $query[1], 6 ); + $slow_query_message .= <<as_error() ) { + // For authentication errors (status 401), include a reminder to set the --user flag. + // WP_CLI::error will only return the first message from WP_Error, so we will pass a string containing both instead. + if ( 401 === $response->get_status() ) { + $errors = $error->get_error_messages(); + $errors[] = __( 'Make sure to include the --user flag with an account that has permissions for this action.', 'woocommerce' ) . ' {"status":401}'; + $error = implode( "\n", $errors ); + } + WP_CLI::error( $error ); + } + return array( $response->get_status(), $response->get_data(), $response->get_headers() ); + } + + /** + * Get Formatter object based on supplied parameters. + * + * @param array $assoc_args Parameters passed to command. Determines formatting. + * @return \WP_CLI\Formatter + */ + protected function get_formatter( &$assoc_args ) { + if ( ! empty( $assoc_args['fields'] ) ) { + if ( is_string( $assoc_args['fields'] ) ) { + $fields = explode( ',', $assoc_args['fields'] ); + } else { + $fields = $assoc_args['fields']; + } + } else { + if ( ! empty( $assoc_args['context'] ) ) { + $fields = $this->get_context_fields( $assoc_args['context'] ); + } else { + $fields = $this->get_context_fields( 'view' ); + } + } + return new \WP_CLI\Formatter( $assoc_args, $fields ); + } + + /** + * Get a list of fields present in a given context + * + * @param string $context + * @return array + */ + private function get_context_fields( $context ) { + $fields = array(); + foreach ( $this->schema['properties'] as $key => $args ) { + if ( empty( $args['context'] ) || in_array( $context, $args['context'] ) ) { + $fields[] = $key; + } + } + return $fields; + } + + /** + * Get the route for this resource + * + * @param array $args + * @return string + */ + private function get_filled_route( $args = array() ) { + $supported_id_matched = false; + $route = $this->route; + + foreach ( $this->get_supported_ids() as $id_name => $id_desc ) { + if ( strpos( $route, '<' . $id_name . '>' ) !== false && ! empty( $args ) ) { + $route = str_replace( '(?P<' . $id_name . '>[\d]+)', $args[0], $route ); + $supported_id_matched = true; + } + } + + if ( ! empty( $args ) ) { + $id_replacement = $supported_id_matched && ! empty( $args[1] ) ? $args[1] : $args[0]; + $route = str_replace( array( '(?P[\d]+)', '(?P[\w-]+)' ), $id_replacement, $route ); + } + + return rtrim( $route ); + } + + /** + * Output a line to be added + * + * @param string + */ + private function add_line( $line ) { + $this->nested_line( $line, 'add' ); + } + + /** + * Output a line to be removed + * + * @param string + */ + private function remove_line( $line ) { + $this->nested_line( $line, 'remove' ); + } + + /** + * Output a line that's appropriately nested + * + * @param string $line + * @param bool|string $change + */ + private function nested_line( $line, $change = false ) { + if ( 'add' == $change ) { + $label = '+ '; + } elseif ( 'remove' == $change ) { + $label = '- '; + } else { + $label = false; + } + + $spaces = ( $this->output_nesting_level * 2 ) + 2; + if ( $label ) { + $line = $label . $line; + $spaces = $spaces - 2; + } + WP_CLI::line( str_pad( ' ', $spaces ) . $line ); + } + + /** + * Whether or not this is an associative array + * + * @param array + * @return bool + */ + private function is_assoc_array( $array ) { + if ( ! is_array( $array ) ) { + return false; + } + return array_keys( $array ) !== range( 0, count( $array ) - 1 ); + } + + /** + * Reduce an item to specific fields. + * + * @param array $item + * @param array $fields + * @return array + */ + private static function limit_item_to_fields( $item, $fields ) { + if ( empty( $fields ) ) { + return $item; + } + if ( is_string( $fields ) ) { + $fields = explode( ',', $fields ); + } + foreach ( $item as $i => $field ) { + if ( ! in_array( $i, $fields ) ) { + unset( $item[ $i ] ); + } + } + return $item; + } + + /** + * JSON can be passed in some more complicated objects, like the payment gateway settings array. + * This function decodes the json (if present) and tries to get it's value. + * + * @param array $arr + * + * @return array + */ + protected function decode_json( $arr ) { + foreach ( $arr as $key => $value ) { + if ( '[' === substr( $value, 0, 1 ) || '{' === substr( $value, 0, 1 ) ) { + $arr[ $key ] = json_decode( $value, true ); + } else { + continue; + } + } + return $arr; + } + +} diff --git a/includes/cli/class-wc-cli-runner.php b/includes/cli/class-wc-cli-runner.php new file mode 100644 index 00000000000..f8b84e4ccbf --- /dev/null +++ b/includes/cli/class-wc-cli-runner.php @@ -0,0 +1,249 @@ +[\w-]+)', + 'settings/(?P[\w-]+)/batch', + 'settings/(?P[\w-]+)/(?P[\w-]+)', + 'system_status', + 'system_status/tools', + 'system_status/tools/(?P[\w-]+)', + 'reports', + 'reports/sales', + 'reports/top_sellers', + ); + + /** + * The version of the REST API we should target to + * generate commands. + */ + private static $target_rest_version = 'v2'; + + /** + * Register's all endpoints as commands once WP and WC have all loaded. + */ + public static function after_wp_load() { + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server; + do_action( 'rest_api_init', $wp_rest_server ); + + $request = new WP_REST_Request( 'GET', '/' ); + $request->set_param( 'context', 'help' ); + $response = $wp_rest_server->dispatch( $request ); + $response_data = $response->get_data(); + if ( empty( $response_data ) ) { + return; + } + + // Loop through all of our endpoints and register any valid WC endpoints. + foreach ( $response_data['routes'] as $route => $route_data ) { + // Only register endpoints for WC and our target version. + if ( substr( $route, 0, 4 + strlen( self::$target_rest_version ) ) !== '/wc/' . self::$target_rest_version ) { + continue; + } + + // Only register endpoints with schemas + if ( empty( $route_data['schema']['title'] ) ) { + WP_CLI::debug( sprintf( __( 'No schema title found for %s, skipping REST command registration.', 'woocommerce' ), $route ), 'wc' ); + continue; + } + // Ignore batch endpoints + if ( 'batch' === $route_data['schema']['title'] ) { + continue; + } + // Disable specific endpoints + $route_pieces = explode( '/', $route ); + $endpoint_piece = str_replace( '/wc/' . $route_pieces[2] . '/', '', $route ); + if ( in_array( $endpoint_piece, self::$disabled_endpoints ) ) { + continue; + } + + self::register_route_commands( new WC_CLI_REST_Command( $route_data['schema']['title'], $route, $route_data['schema'] ), $route, $route_data ); + } + } + + /** + * Generates command information and tells WP CLI about all + * commands avaiable from a route. + * + * @param string $rest_command + * @param string $route + * @param array $route_data + * @param array $command_args + */ + private static function register_route_commands( $rest_command, $route, $route_data, $command_args = array() ) { + // Define IDs that we are looking for in the routes (in addition to id) + // so that we can pass it to the rest command, and use it here to generate documentation. + $supported_ids = array( + 'id' => __( 'ID.', 'woocommerce' ), + 'product_id' => __( 'Product ID.', 'woocommerce' ), + 'customer_id' => __( 'Customer ID.', 'woocommerce' ), + 'order_id' => __( 'Order ID.', 'woocommerce' ), + 'refund_id' => __( 'Refund ID.', 'woocommerce' ), + 'attribute_id' => __( 'Attribute ID.', 'woocommerce' ), + 'zone_id' => __( 'Zone ID.', 'woocommerce' ), + ); + $rest_command->set_supported_ids( $supported_ids ); + $positional_args = array_keys( $supported_ids ); + + $parent = "wc {$route_data['schema']['title']}"; + $supported_commands = array(); + + // Get a list of supported commands for each route. + foreach ( $route_data['endpoints'] as $endpoint ) { + preg_match_all( '#\([^\)]+\)#', $route, $matches ); + $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; + $trimmed_route = rtrim( $route ); + $is_singular = substr( $trimmed_route, - strlen( $resource_id ) ) === $resource_id; + + // List a collection + if ( array( 'GET' ) == $endpoint['methods'] && ! $is_singular ) { + $supported_commands['list'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + // Create a specific resource + if ( array( 'POST' ) == $endpoint['methods'] && ! $is_singular ) { + $supported_commands['create'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + // Get a specific resource + if ( array( 'GET' ) == $endpoint['methods'] && $is_singular ) { + $supported_commands['get'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + // Update a specific resource + if ( in_array( 'POST', $endpoint['methods'] ) && $is_singular ) { + $supported_commands['update'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + // Delete a specific resource + if ( array( 'DELETE' ) == $endpoint['methods'] && $is_singular ) { + $supported_commands['delete'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + } + } + + foreach ( $supported_commands as $command => $endpoint_args ) { + $synopsis = array(); + $arg_regs = array(); + $ids = array(); + + foreach ( $supported_ids as $id_name => $id_desc ) { + if ( strpos( $route, '<' . $id_name . '>' ) !== false ) { + $synopsis[] = array( + 'name' => $id_name, + 'type' => 'positional', + 'description' => $id_desc, + 'optional' => false, + ); + $ids[] = $id_name; + } + } + if ( in_array( $command, array( 'delete', 'get', 'update' ) ) && ! in_array( 'id', $ids ) ) { + $synopsis[] = array( + 'name' => 'id', + 'type' => 'positional', + 'description' => __( 'The id for the resource.', 'woocommerce' ), + 'optional' => false, + ); + } + + foreach ( $endpoint_args as $name => $args ) { + if ( ! in_array( $name, $positional_args ) || strpos( $route, '<' . $id_name . '>' ) === false ) { + $arg_regs[] = array( + 'name' => $name, + 'type' => 'assoc', + 'description' => ! empty( $args['description'] ) ? $args['description'] : '', + 'optional' => empty( $args['required'] ) ? true : false, + ); + } + } + + foreach ( $arg_regs as $arg_reg ) { + $synopsis[] = $arg_reg; + } + + if ( in_array( $command, array( 'list', 'get' ) ) ) { + $synopsis[] = array( + 'name' => 'fields', + 'type' => 'assoc', + 'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ), + 'optional' => true, + ); + $synopsis[] = array( + 'name' => 'field', + 'type' => 'assoc', + 'description' => __( 'Get the value of an individual field.', 'woocommerce' ), + 'optional' => true, + ); + $synopsis[] = array( + 'name' => 'format', + 'type' => 'assoc', + 'description' => __( 'Render response in a particular format.', 'woocommerce' ), + 'optional' => true, + 'default' => 'table', + 'options' => array( + 'table', + 'json', + 'csv', + 'ids', + 'yaml', + 'count', + 'headers', + 'body', + 'envelope', + ), + ); + } + + if ( in_array( $command, array( 'create', 'update', 'delete' ) ) ) { + $synopsis[] = array( + 'name' => 'porcelain', + 'type' => 'flag', + 'description' => __( 'Output just the id when the operation is successful.', 'woocommerce' ), + 'optional' => true, + ); + } + + $methods = array( + 'list' => 'list_items', + 'create' => 'create_item', + 'delete' => 'delete_item', + 'get' => 'get_item', + 'update' => 'update_item', + ); + + $before_invoke = null; + if ( empty( $command_args['when'] ) && \WP_CLI::get_config( 'debug' ) ) { + $before_invoke = function() { + if ( ! defined( 'SAVEQUERIES' ) ) { + define( 'SAVEQUERIES', true ); + } + }; + } + + WP_CLI::add_command( "{$parent} {$command}", array( $rest_command, $methods[ $command ] ), array( + 'synopsis' => $synopsis, + 'when' => ! empty( $command_args['when'] ) ? $command_args['when'] : '', + 'before_invoke' => $before_invoke, + ) ); + } + } +} diff --git a/includes/cli/class-wc-cli-tool-command.php b/includes/cli/class-wc-cli-tool-command.php new file mode 100644 index 00000000000..f2859cd4b12 --- /dev/null +++ b/includes/cli/class-wc-cli-tool-command.php @@ -0,0 +1,100 @@ +dispatch( $request ); + $response_data = $response->get_data(); + if ( empty( $response_data ) ) { + return; + } + + $parent = "wc tool"; + $supported_commands = array( 'list', 'run' ); + foreach ( $supported_commands as $command ) { + $synopsis = array(); + if ( 'run' === $command ) { + $synopsis[] = array( + 'name' => 'id', + 'type' => 'positional', + 'description' => __( 'The id for the resource.', 'woocommerce' ), + 'optional' => false, + ); + $method = 'update_item'; + $route = '/wc/v1/system_status/tools/(?P[\w-]+)'; + } elseif ( 'list' === $command ) { + $synopsis[] = array( + 'name' => 'fields', + 'type' => 'assoc', + 'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ), + 'optional' => true, + ); + $synopsis[] = array( + 'name' => 'field', + 'type' => 'assoc', + 'description' => __( 'Get the value of an individual field.', 'woocommerce' ), + 'optional' => true, + ); + $synopsis[] = array( + 'name' => 'format', + 'type' => 'assoc', + 'description' => __( 'Render response in a particular format.', 'woocommerce' ), + 'optional' => true, + 'default' => 'table', + 'options' => array( + 'table', + 'json', + 'csv', + 'ids', + 'yaml', + 'count', + 'headers', + 'body', + 'envelope', + ), + ); + $method = 'list_items'; + $route = '/wc/v1/system_status/tools'; + } + + $before_invoke = null; + if ( empty( $command_args['when'] ) && WP_CLI::get_config( 'debug' ) ) { + $before_invoke = function() { + if ( ! defined( 'SAVEQUERIES' ) ) { + define( 'SAVEQUERIES', true ); + } + }; + } + + $rest_command = new WC_CLI_REST_Command( 'system_status_tool', $route, $response_data['schema'] ); + + WP_CLI::add_command( "{$parent} {$command}", array( $rest_command, $method ), array( + 'synopsis' => $synopsis, + 'when' => ! empty( $command_args['when'] ) ? $command_args['when'] : '', + 'before_invoke' => $before_invoke, + ) ); + } + } + +} diff --git a/includes/cli/class-wc-cli-update-command.php b/includes/cli/class-wc-cli-update-command.php new file mode 100644 index 00000000000..e338f36fbbb --- /dev/null +++ b/includes/cli/class-wc-cli-update-command.php @@ -0,0 +1,49 @@ +hide_errors(); + + include_once( WC_ABSPATH . 'includes/class-wc-install.php' ); + include_once( WC_ABSPATH . 'includes/wc-update-functions.php' ); + + $current_db_version = get_option( 'woocommerce_db_version' ); + $update_count = 0; + + foreach ( WC_Install::get_db_update_callbacks() as $version => $update_callbacks ) { + if ( version_compare( $current_db_version, $version, '<' ) ) { + foreach ( $update_callbacks as $update_callback ) { + WP_CLI::log( sprintf( __( 'Calling update function: %s', 'woocommerce' ), $update_callback ) ); + call_user_func( $update_callback ); + $update_count ++; + } + } + } + + WC_Admin_Notices::remove_notice( 'update' ); + WP_CLI::success( sprintf( __( '%1$d updates complete. Database version is %2$s', 'woocommerce' ), absint( $update_count ), get_option( 'woocommerce_db_version' ) ) ); + } +} diff --git a/includes/data-stores/abstract-wc-order-data-store-cpt.php b/includes/data-stores/abstract-wc-order-data-store-cpt.php new file mode 100644 index 00000000000..4ba1553ed87 --- /dev/null +++ b/includes/data-stores/abstract-wc-order-data-store-cpt.php @@ -0,0 +1,361 @@ +set_version( WC_VERSION ); + $order->set_date_created( current_time( 'timestamp', true ) ); + $order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() ); + + $id = wp_insert_post( apply_filters( 'woocommerce_new_order_data', array( + 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + 'post_type' => $order->get_type( 'edit' ), + 'post_status' => 'wc-' . ( $order->get_status( 'edit' ) ? $order->get_status( 'edit' ) : apply_filters( 'woocommerce_default_order_status', 'pending' ) ), + 'ping_status' => 'closed', + 'post_author' => 1, + 'post_title' => $this->get_post_title(), + 'post_password' => uniqid( 'order_' ), + 'post_parent' => $order->get_parent_id( 'edit' ), + 'post_excerpt' => $this->get_post_excerpt( $order ), + ) ), true ); + + if ( $id && ! is_wp_error( $id ) ) { + $order->set_id( $id ); + $this->update_post_meta( $order ); + $order->save_meta_data(); + $order->apply_changes(); + $this->clear_caches( $order ); + } + } + + /** + * Method to read an order from the database. + * + * @param WC_Data $order + * + * @throws Exception + */ + public function read( &$order ) { + $order->set_defaults(); + + if ( ! $order->get_id() || ! ( $post_object = get_post( $order->get_id() ) ) || ! in_array( $post_object->post_type, wc_get_order_types() ) ) { + throw new Exception( __( 'Invalid order.', 'woocommerce' ) ); + } + + $order->set_props( array( + 'parent_id' => $post_object->post_parent, + 'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null, + 'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null, + 'status' => $post_object->post_status, + ) ); + + $this->read_order_data( $order, $post_object ); + $order->read_meta_data(); + $order->set_object_read( true ); + + /** + * In older versions, discounts may have been stored differently. + * Update them now so if the object is saved, the correct values are + * stored. @todo When meta is flattened, handle this during migration. + */ + if ( version_compare( $order->get_version( 'edit' ), '2.3.7', '<' ) && $order->get_prices_include_tax( 'edit' ) ) { + $order->set_discount_total( (double) get_post_meta( $order->get_id(), '_cart_discount', true ) - (double) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) ); + } + } + + /** + * Method to update an order in the database. + * @param WC_Order $order + */ + public function update( &$order ) { + $order->save_meta_data(); + $order->set_version( WC_VERSION ); + + $changes = $order->get_changes(); + + // Only update the post when the post data changes. + if ( array_intersect( array( 'date_created', 'date_modified', 'status', 'parent_id', 'post_excerpt' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + 'post_status' => 'wc-' . ( $order->get_status( 'edit' ) ? $order->get_status( 'edit' ) : apply_filters( 'woocommerce_default_order_status', 'pending' ) ), + 'post_parent' => $order->get_parent_id(), + 'post_excerpt' => $this->get_post_excerpt( $order ), + 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), + 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), + ); + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $order->get_id() ) ); + clean_post_cache( $order->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $order->get_id() ), $post_data ) ); + } + $order->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + } + $this->update_post_meta( $order ); + $order->apply_changes(); + $this->clear_caches( $order ); + } + + /** + * Method to delete an order from the database. + * @param WC_Order $order + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$order, $args = array() ) { + $id = $order->get_id(); + $args = wp_parse_args( $args, array( + 'force_delete' => false, + ) ); + + if ( ! $id ) { + return; + } + + if ( $args['force_delete'] ) { + wp_delete_post( $id ); + $order->set_id( 0 ); + do_action( 'woocommerce_delete_order', $id ); + } else { + wp_trash_post( $id ); + $order->set_status( 'trash' ); + do_action( 'woocommerce_trash_order', $id ); + } + } + + /* + |-------------------------------------------------------------------------- + | Additional Methods + |-------------------------------------------------------------------------- + */ + + /** + * Excerpt for post. + * + * @param WC_order $order + * @return string + */ + protected function get_post_excerpt( $order ) { + return ''; + } + + /** + * Get a title for the new post type. + * + * @return string + */ + protected function get_post_title() { + // @codingStandardsIgnoreStart + /* translators: %s: Order date */ + return sprintf( __( 'Order – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); + // @codingStandardsIgnoreEnd + } + + /** + * Read order data. Can be overridden by child classes to load other props. + * + * @param WC_Order + * @param object $post_object + * @since 3.0.0 + */ + protected function read_order_data( &$order, $post_object ) { + $id = $order->get_id(); + + $order->set_props( array( + 'currency' => get_post_meta( $id, '_order_currency', true ), + 'discount_total' => get_post_meta( $id, '_cart_discount', true ), + 'discount_tax' => get_post_meta( $id, '_cart_discount_tax', true ), + 'shipping_total' => get_post_meta( $id, '_order_shipping', true ), + 'shipping_tax' => get_post_meta( $id, '_order_shipping_tax', true ), + 'cart_tax' => get_post_meta( $id, '_order_tax', true ), + 'total' => get_post_meta( $id, '_order_total', true ), + 'version' => get_post_meta( $id, '_order_version', true ), + 'prices_include_tax' => metadata_exists( 'post', $id, '_prices_include_tax' ) ? 'yes' === get_post_meta( $id, '_prices_include_tax', true ) : 'yes' === get_option( 'woocommerce_prices_include_tax' ), + ) ); + + // Gets extra data associated with the order if needed. + foreach ( $order->get_extra_data_keys() as $key ) { + $function = 'set_' . $key; + if ( is_callable( array( $order, $function ) ) ) { + $order->{$function}( get_post_meta( $order->get_id(), '_' . $key, true ) ); + } + } + } + + /** + * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. + * + * @param $order WC_Order + * @since 3.0.0 + */ + protected function update_post_meta( &$order ) { + $updated_props = array(); + $meta_key_to_props = array( + '_order_currency' => 'currency', + '_cart_discount' => 'discount_total', + '_cart_discount_tax' => 'discount_tax', + '_order_shipping' => 'shipping_total', + '_order_shipping_tax' => 'shipping_tax', + '_order_tax' => 'cart_tax', + '_order_total' => 'total', + '_order_version' => 'version', + '_prices_include_tax' => 'prices_include_tax', + ); + + $props_to_update = $this->get_props_to_update( $order, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + + if ( 'prices_include_tax' === $prop ) { + $value = $value ? 'yes' : 'no'; + } + + if ( update_post_meta( $order->get_id(), $meta_key, $value ) ) { + $updated_props[] = $prop; + } + } + + do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); + } + + /** + * Clear any caches. + * + * @param WC_Order + * @since 3.0.0 + */ + protected function clear_caches( &$order ) { + clean_post_cache( $order->get_id() ); + wc_delete_shop_order_transients( $order ); + wp_cache_delete( 'order-items-' . $order->get_id(), 'orders' ); + } + + /** + * Read order items of a specific type from the database for this order. + * + * @param WC_Order $order + * @param string $type + * @return array + */ + public function read_items( $order, $type ) { + global $wpdb; + + // Get from cache if available. + $items = wp_cache_get( 'order-items-' . $order->get_id(), 'orders' ); + + if ( false === $items ) { + $get_items_sql = $wpdb->prepare( "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d ORDER BY order_item_id;", $order->get_id() ); + $items = $wpdb->get_results( $get_items_sql ); + foreach ( $items as $item ) { + wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' ); + } + wp_cache_set( 'order-items-' . $order->get_id(), $items, 'orders' ); + } + + $items = wp_list_filter( $items, array( 'order_item_type' => $type ) ); + + if ( ! empty( $items ) ) { + $items = array_map( array( 'WC_Order_Factory', 'get_order_item' ), array_combine( wp_list_pluck( $items, 'order_item_id' ), $items ) ); + } else { + $items = array(); + } + + return $items; + } + + /** + * Remove all line items (products, coupons, shipping, taxes) from the order. + * + * @param WC_Order + * @param string $type Order item type. Default null. + */ + public function delete_items( $order, $type = null ) { + global $wpdb; + if ( ! empty( $type ) ) { + $wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id AND items.order_id = %d AND items.order_item_type = %s", $order->get_id(), $type ) ); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_type = %s", $order->get_id(), $type ) ); + } else { + $wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id and items.order_id = %d", $order->get_id() ) ); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order->get_id() ) ); + } + $this->clear_caches( $order ); + } + + /** + * Get token ids for an order. + * + * @param WC_Order + * @return array + */ + public function get_payment_token_ids( $order ) { + $token_ids = array_filter( (array) get_post_meta( $order->get_id(), '_payment_tokens', true ) ); + return $token_ids; + } + + /** + * Update token ids for an order. + * + * @param WC_Order + * @param array $token_ids + */ + public function update_payment_token_ids( $order, $token_ids ) { + update_post_meta( $order->get_id(), '_payment_tokens', $token_ids ); + } +} diff --git a/includes/data-stores/abstract-wc-order-item-type-data-store.php b/includes/data-stores/abstract-wc-order-item-type-data-store.php new file mode 100644 index 00000000000..c568ad918a5 --- /dev/null +++ b/includes/data-stores/abstract-wc-order-item-type-data-store.php @@ -0,0 +1,147 @@ +insert( $wpdb->prefix . 'woocommerce_order_items', array( + 'order_item_name' => $item->get_name(), + 'order_item_type' => $item->get_type(), + 'order_id' => $item->get_order_id(), + ) ); + $item->set_id( $wpdb->insert_id ); + $this->save_item_data( $item ); + $item->save_meta_data(); + $item->apply_changes(); + $this->clear_cache( $item ); + + do_action( 'woocommerce_new_order_item', $item->get_id(), $item, $item->get_order_id() ); + } + + /** + * Update a order item in the database. + * + * @since 3.0.0 + * @param WC_Order_Item $item + */ + public function update( &$item ) { + global $wpdb; + + $changes = $item->get_changes(); + + if ( array_intersect( array( 'name', 'order_id' ), array_keys( $changes ) ) ) { + $wpdb->update( $wpdb->prefix . 'woocommerce_order_items', array( + 'order_item_name' => $item->get_name(), + 'order_item_type' => $item->get_type(), + 'order_id' => $item->get_order_id(), + ), array( 'order_item_id' => $item->get_id() ) ); + } + + $this->save_item_data( $item ); + $item->save_meta_data(); + $item->apply_changes(); + $this->clear_cache( $item ); + + do_action( 'woocommerce_update_order_item', $item->get_id(), $item, $item->get_order_id() ); + } + + /** + * Remove an order item from the database. + * + * @since 3.0.0 + * @param WC_Order_Item $item + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$item, $args = array() ) { + if ( $item->get_id() ) { + global $wpdb; + do_action( 'woocommerce_before_delete_order_item', $item->get_id() ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_order_items', array( 'order_item_id' => $item->get_id() ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_order_itemmeta', array( 'order_item_id' => $item->get_id() ) ); + do_action( 'woocommerce_delete_order_item', $item->get_id() ); + } + } + + /** + * Read a order item from the database. + * + * @since 3.0.0 + * + * @param WC_Order_Item $item + * + * @throws Exception + */ + public function read( &$item ) { + global $wpdb; + + $item->set_defaults(); + + // Get from cache if available. + $data = wp_cache_get( 'item-' . $item->get_id(), 'order-items' ); + + if ( false === $data ) { + $data = $wpdb->get_row( $wpdb->prepare( "SELECT order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;", $item->get_id() ) ); + wp_cache_set( 'item-' . $item->get_id(), $data, 'order-items' ); + } + + if ( ! $data ) { + throw new Exception( __( 'Invalid order item.', 'woocommerce' ) ); + } + + $item->set_props( array( + 'order_id' => $data->order_id, + 'name' => $data->order_item_name, + ) ); + $item->read_meta_data(); + } + + /** + * Saves an item's data to the database / item meta. + * Ran after both create and update, so $item->get_id() will be set. + * + * @since 3.0.0 + * @param WC_Order_Item $item + */ + public function save_item_data( &$item ) {} + + /** + * Clear meta cachce. + * + * @param WC_Order_Item $item + */ + public function clear_cache( &$item ) { + wp_cache_delete( 'item-' . $item->get_id(), 'order-items' ); + } +} diff --git a/includes/data-stores/class-wc-coupon-data-store-cpt.php b/includes/data-stores/class-wc-coupon-data-store-cpt.php new file mode 100644 index 00000000000..cbeebcb749f --- /dev/null +++ b/includes/data-stores/class-wc-coupon-data-store-cpt.php @@ -0,0 +1,365 @@ +set_date_created( current_time( 'timestamp', true ) ); + + $coupon_id = wp_insert_post( apply_filters( 'woocommerce_new_coupon_data', array( + 'post_type' => 'shop_coupon', + 'post_status' => 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $coupon->get_code( 'edit' ), + 'post_content' => '', + 'post_excerpt' => $coupon->get_description( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ), + ) ), true ); + + if ( $coupon_id ) { + $coupon->set_id( $coupon_id ); + $this->update_post_meta( $coupon ); + $coupon->save_meta_data(); + $coupon->apply_changes(); + do_action( 'woocommerce_new_coupon', $coupon_id ); + } + } + + /** + * Method to read a coupon. + * + * @since 3.0.0 + * + * @param WC_Data $coupon + * + * @throws Exception + */ + public function read( &$coupon ) { + $coupon->set_defaults(); + + if ( ! $coupon->get_id() || ! ( $post_object = get_post( $coupon->get_id() ) ) || 'shop_coupon' !== $post_object->post_type ) { + throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) ); + } + + $coupon_id = $coupon->get_id(); + $coupon->set_props( array( + 'code' => $post_object->post_title, + 'description' => $post_object->post_excerpt, + 'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null, + 'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null, + 'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), + 'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ), + 'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ), + 'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ), + 'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ), + 'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ), + 'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ), + 'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ), + 'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ), + 'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null, + 'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ), + 'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ), + 'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ), + 'exclude_sale_items' => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ), + 'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ), + 'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ), + 'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ), + 'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ), + ) ); + $coupon->read_meta_data(); + $coupon->set_object_read( true ); + do_action( 'woocommerce_coupon_loaded', $coupon ); + } + + /** + * Updates a coupon in the database. + * + * @since 3.0.0 + * @param WC_Coupon $coupon + */ + public function update( &$coupon ) { + $coupon->save_meta_data(); + $changes = $coupon->get_changes(); + + if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_title' => $coupon->get_code( 'edit' ), + 'post_excerpt' => $coupon->get_description( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ), + 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), + 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), + ); + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) ); + clean_post_cache( $coupon->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) ); + } + $coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + } + $this->update_post_meta( $coupon ); + $coupon->apply_changes(); + do_action( 'woocommerce_update_coupon', $coupon->get_id() ); + } + + /** + * Deletes a coupon from the database. + * + * @since 3.0.0 + * + * @param WC_Coupon $coupon + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$coupon, $args = array() ) { + $args = wp_parse_args( $args, array( + 'force_delete' => false, + ) ); + + $id = $coupon->get_id(); + + if ( ! $id ) { + return; + } + + if ( $args['force_delete'] ) { + wp_delete_post( $id ); + $coupon->set_id( 0 ); + do_action( 'woocommerce_delete_coupon', $id ); + } else { + wp_trash_post( $id ); + do_action( 'woocommerce_trash_coupon', $id ); + } + } + + /** + * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class. + * + * @param WC_Coupon + * @since 3.0.0 + */ + private function update_post_meta( &$coupon ) { + $updated_props = array(); + $meta_key_to_props = array( + 'discount_type' => 'discount_type', + 'coupon_amount' => 'amount', + 'individual_use' => 'individual_use', + 'product_ids' => 'product_ids', + 'exclude_product_ids' => 'excluded_product_ids', + 'usage_limit' => 'usage_limit', + 'usage_limit_per_user' => 'usage_limit_per_user', + 'limit_usage_to_x_items' => 'limit_usage_to_x_items', + 'usage_count' => 'usage_count', + 'date_expires' => 'date_expires', + 'free_shipping' => 'free_shipping', + 'product_categories' => 'product_categories', + 'exclude_product_categories' => 'excluded_product_categories', + 'exclude_sale_items' => 'exclude_sale_items', + 'minimum_amount' => 'minimum_amount', + 'maximum_amount' => 'maximum_amount', + 'customer_email' => 'email_restrictions', + ); + + $props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props ); + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $coupon->{"get_$prop"}( 'edit' ); + switch ( $prop ) { + case 'individual_use' : + case 'free_shipping' : + case 'exclude_sale_items' : + $updated = update_post_meta( $coupon->get_id(), $meta_key, wc_bool_to_string( $value ) ); + break; + case 'product_ids' : + case 'excluded_product_ids' : + $updated = update_post_meta( $coupon->get_id(), $meta_key, implode( ',', array_filter( array_map( 'intval', $value ) ) ) ); + break; + case 'product_categories' : + case 'excluded_product_categories' : + $updated = update_post_meta( $coupon->get_id(), $meta_key, array_filter( array_map( 'intval', $value ) ) ); + break; + case 'email_restrictions' : + $updated = update_post_meta( $coupon->get_id(), $meta_key, array_filter( array_map( 'sanitize_email', $value ) ) ); + break; + case 'date_expires' : + $updated = update_post_meta( $coupon->get_id(), $meta_key, ( $value ? $value->getTimestamp() : null ) ); + update_post_meta( $coupon->get_id(), 'expiry_date', ( $value ? $value->date( 'Y-m-d' ) : '' ) ); // Update the old meta key for backwards compatibility. + break; + default : + $updated = update_post_meta( $coupon->get_id(), $meta_key, $value ); + break; + } + if ( $updated ) { + $updated_props[] = $prop; + } + } + + do_action( 'woocommerce_coupon_object_updated_props', $coupon, $updated_props ); + } + + /** + * Increase usage count for current coupon. + * + * @since 3.0.0 + * @param WC_Coupon + * @param string $used_by Either user ID or billing email + * @return int New usage count + */ + public function increase_usage_count( &$coupon, $used_by = '' ) { + $new_count = $this->update_usage_count_meta( $coupon, 'increase' ); + if ( $used_by ) { + add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); + $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); + } + return $new_count; + } + + /** + * Decrease usage count for current coupon. + * + * @since 3.0.0 + * @param WC_Coupon + * @param string $used_by Either user ID or billing email + * @return int New usage count + */ + public function decrease_usage_count( &$coupon, $used_by = '' ) { + global $wpdb; + $new_count = $this->update_usage_count_meta( $coupon, 'decrease' ); + if ( $used_by ) { + /** + * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes. + * all instances where the key and value match, and we only want to delete one. + */ + $meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", $used_by, $coupon->get_id() ) ); + if ( $meta_id ) { + delete_metadata_by_mid( 'post', $meta_id ); + $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); + } + } + return $new_count; + } + + /** + * Increase or decrease the usage count for a coupon by 1. + * + * @since 3.0.0 + * @param WC_Coupon + * @param string $operation 'increase' or 'decrease' + * @return int New usage count + */ + private function update_usage_count_meta( &$coupon, $operation = 'increase' ) { + global $wpdb; + $id = $coupon->get_id(); + $operator = ( 'increase' === $operation ) ? '+' : '-'; + + add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true ); + $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) ); + + // Get the latest value direct from the DB, instead of possibly the WP meta cache. + return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) ); + } + + /** + * Get the number of uses for a coupon by user ID. + * + * @since 3.0.0 + * @param WC_Coupon + * @param id $user_id + * @return int + */ + public function get_usage_by_user_id( &$coupon, $user_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", $coupon->get_id(), $user_id ) ); + } + + /** + * Return a coupon code for a specific ID. + * + * @since 3.0.0 + * @param int $id + * @return string Coupon Code + */ + public function get_code_by_id( $id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( " + SELECT post_title + FROM $wpdb->posts + WHERE ID = %d + AND post_type = 'shop_coupon' + AND post_status = 'publish'; + ", $id ) ); + } + + /** + * Return an array of IDs for for a specific coupon code. + * Can return multiple to check for existence. + * + * @since 3.0.0 + * @param string $code + * @return array Array of IDs. + */ + public function get_ids_by_code( $code ) { + global $wpdb; + return $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC;", $code ) ); + } +} diff --git a/includes/data-stores/class-wc-customer-data-store-session.php b/includes/data-stores/class-wc-customer-data-store-session.php new file mode 100644 index 00000000000..bbce155c2a7 --- /dev/null +++ b/includes/data-stores/class-wc-customer-data-store-session.php @@ -0,0 +1,183 @@ +save_to_session( $customer ); + } + + /** + * Simply update the session. + * + * @param WC_Customer $customer + */ + public function update( &$customer ) { + $this->save_to_session( $customer ); + } + + /** + * Saves all customer data to the session. + * + * @param WC_Customer $customer + */ + public function save_to_session( $customer ) { + $data = array(); + foreach ( $this->session_keys as $session_key ) { + $function_key = $session_key; + if ( 'billing_' === substr( $session_key, 0, 8 ) ) { + $session_key = str_replace( 'billing_', '', $session_key ); + } + $data[ $session_key ] = $customer->{"get_$function_key"}( 'edit' ); + } + if ( WC()->session->get( 'customer' ) !== $data ) { + WC()->session->set( 'customer', $data ); + } + } + + /** + * Read customer data from the session unless the user has logged in, in + * which case the stored ID will differ from the actual ID. + * + * @since 3.0.0 + * @param WC_Customer $customer + */ + public function read( &$customer ) { + $data = (array) WC()->session->get( 'customer' ); + if ( ! empty( $data ) && isset( $data['id'] ) && $data['id'] === $customer->get_id() ) { + foreach ( $this->session_keys as $session_key ) { + $function_key = $session_key; + if ( 'billing_' === substr( $session_key, 0, 8 ) ) { + $session_key = str_replace( 'billing_', '', $session_key ); + } + if ( ! empty( $data[ $session_key ] ) && is_callable( array( $customer, "set_{$function_key}" ) ) ) { + $customer->{"set_{$function_key}"}( $data[ $session_key ] ); + } + } + } + $this->set_defaults( $customer ); + $customer->set_object_read( true ); + } + + /** + * Load default values if props are unset. + * + * @param WC_Customer $customer + */ + protected function set_defaults( &$customer ) { + try { + $default = wc_get_customer_default_location(); + + if ( ! $customer->get_billing_country() ) { + $customer->set_billing_country( $default['country'] ); + } + + if ( ! $customer->get_shipping_country() ) { + $customer->set_shipping_country( $customer->get_billing_country() ); + } + + if ( ! $customer->get_billing_state() ) { + $customer->set_billing_state( $default['state'] ); + } + + if ( ! $customer->get_shipping_state() ) { + $customer->set_shipping_state( $customer->get_billing_state() ); + } + + if ( ! $customer->get_billing_email() && is_user_logged_in() ) { + $current_user = wp_get_current_user(); + $customer->set_billing_email( $current_user->user_email ); + } + } catch ( WC_Data_Exception $e ) {} + } + + /** + * Deletes a customer from the database. + * + * @since 3.0.0 + * @param WC_Customer $customer + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$customer, $args = array() ) { + WC()->session->set( 'customer', null ); + } + + /** + * Gets the customers last order. + * + * @since 3.0.0 + * @param WC_Customer + * @return WC_Order|false + */ + public function get_last_order( &$customer ) { + return false; + } + + /** + * Return the number of orders this customer has. + * + * @since 3.0.0 + * @param WC_Customer + * @return integer + */ + public function get_order_count( &$customer ) { + return 0; + } + + /** + * Return how much money this customer has spent. + * + * @since 3.0.0 + * @param WC_Customer + * @return float + */ + public function get_total_spent( &$customer ) { + return 0; + } +} diff --git a/includes/data-stores/class-wc-customer-data-store.php b/includes/data-stores/class-wc-customer-data-store.php new file mode 100644 index 00000000000..a3d54e04611 --- /dev/null +++ b/includes/data-stores/class-wc-customer-data-store.php @@ -0,0 +1,410 @@ +prefix ? $wpdb->prefix : 'wp_'; + + return ! in_array( $meta->meta_key, $this->internal_meta_keys ) + && 0 !== strpos( $meta->meta_key, '_woocommerce_persistent_cart' ) + && 0 !== strpos( $meta->meta_key, 'closedpostboxes_' ) + && 0 !== strpos( $meta->meta_key, 'metaboxhidden_' ) + && 0 !== strpos( $meta->meta_key, 'manageedit-' ) + && ! strstr( $meta->meta_key, $table_prefix ) + && 0 !== stripos( $meta->meta_key, 'wp_' ); + } + + /** + * Method to create a new customer in the database. + * + * @since 3.0.0 + * + * @param WC_Data $customer + * + * @throws WC_Data_Exception + */ + public function create( &$customer ) { + $id = wc_create_new_customer( $customer->get_email(), $customer->get_username(), $customer->get_password() ); + + if ( is_wp_error( $id ) ) { + throw new WC_Data_Exception( $id->get_error_code(), $id->get_error_message() ); + } + + $customer->set_id( $id ); + $this->update_user_meta( $customer ); + + // Prevent wp_update_user calls in the same request and customer trigger the 'Notice of Password Changed' email + $customer->set_password( '' ); + + wp_update_user( apply_filters( 'woocommerce_update_customer_args', array( + 'ID' => $customer->get_id(), + 'role' => $customer->get_role(), + 'display_name' => $customer->get_first_name() . ' ' . $customer->get_last_name(), + ), $customer ) ); + $wp_user = new WP_User( $customer->get_id() ); + $customer->set_date_created( $wp_user->user_registered ); + $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); + $customer->save_meta_data(); + $customer->apply_changes(); + do_action( 'woocommerce_new_customer', $customer->get_id() ); + } + + /** + * Method to read a customer object. + * + * @since 3.0.0 + * @param WC_Customer $customer + * @throws Exception + */ + public function read( &$customer ) { + // User object is required. + if ( ! $customer->get_id() || ! ( $user_object = get_user_by( 'id', $customer->get_id() ) ) || empty( $user_object->ID ) ) { + throw new Exception( __( 'Invalid customer.', 'woocommerce' ) ); + } + + // Only users on this site should be read. + if ( is_multisite() && ! is_user_member_of_blog( $customer->get_id() ) ) { + throw new Exception( __( 'Invalid customer.', 'woocommerce' ) ); + } + + $customer_id = $customer->get_id(); + // Load meta but exclude deprecated props. + $user_meta = array_diff_key( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ), array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ) ); + $customer->set_props( $user_meta ); + $customer->set_props( array( + 'is_paying_customer' => get_user_meta( $customer_id, 'paying_customer', true ), + 'email' => $user_object->user_email, + 'username' => $user_object->user_login, + 'display_name' => $user_object->display_name, + 'date_created' => $user_object->user_registered, // Mysql string in local format. + 'date_modified' => get_user_meta( $customer_id, 'last_update', true ), + 'role' => ! empty( $user_object->roles[0] ) ? $user_object->roles[0] : 'customer', + ) ); + $customer->read_meta_data(); + $customer->set_object_read( true ); + do_action( 'woocommerce_customer_loaded', $customer ); + } + + /** + * Updates a customer in the database. + * + * @since 3.0.0 + * @param WC_Customer $customer + */ + public function update( &$customer ) { + wp_update_user( apply_filters( 'woocommerce_update_customer_args', array( + 'ID' => $customer->get_id(), + 'user_email' => $customer->get_email(), + 'display_name' => $customer->get_display_name(), + ), $customer ) ); + // Only update password if a new one was set with set_password. + if ( $customer->get_password() ) { + wp_update_user( array( 'ID' => $customer->get_id(), 'user_pass' => $customer->get_password() ) ); + $customer->set_password( '' ); + } + $this->update_user_meta( $customer ); + $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); + $customer->save_meta_data(); + $customer->apply_changes(); + do_action( 'woocommerce_update_customer', $customer->get_id() ); + } + + /** + * Deletes a customer from the database. + * + * @since 3.0.0 + * @param WC_Customer $customer + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$customer, $args = array() ) { + if ( ! $customer->get_id() ) { + return; + } + $args = wp_parse_args( $args, array( + 'reassign' => 0, + ) ); + + $id = $customer->get_id(); + wp_delete_user( $id, $args['reassign'] ); + + do_action( 'woocommerce_delete_customer', $id ); + } + + /** + * Helper method that updates all the meta for a customer. Used for update & create. + * @since 3.0.0 + * @param WC_Customer + */ + private function update_user_meta( $customer ) { + $updated_props = array(); + $changed_props = $customer->get_changes(); + + $meta_key_to_props = array( + 'paying_customer' => 'is_paying_customer', + 'first_name' => 'first_name', + 'last_name' => 'last_name', + ); + + foreach ( $meta_key_to_props as $meta_key => $prop ) { + if ( ! array_key_exists( $prop, $changed_props ) ) { + continue; + } + + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + $billing_address_props = array( + 'billing_first_name' => 'billing_first_name', + 'billing_last_name' => 'billing_last_name', + 'billing_company' => 'billing_company', + 'billing_address_1' => 'billing_address_1', + 'billing_address_2' => 'billing_address_2', + 'billing_city' => 'billing_city', + 'billing_state' => 'billing_state', + 'billing_postcode' => 'billing_postcode', + 'billing_country' => 'billing_country', + 'billing_email' => 'billing_email', + 'billing_phone' => 'billing_phone', + ); + + foreach ( $billing_address_props as $meta_key => $prop ) { + $prop_key = substr( $prop, 8 ); + if ( ! isset( $changed_props['billing'] ) || ! array_key_exists( $prop_key, $changed_props['billing'] ) ) { + continue; + } + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + $shipping_address_props = array( + 'shipping_first_name' => 'shipping_first_name', + 'shipping_last_name' => 'shipping_last_name', + 'shipping_company' => 'shipping_company', + 'shipping_address_1' => 'shipping_address_1', + 'shipping_address_2' => 'shipping_address_2', + 'shipping_city' => 'shipping_city', + 'shipping_state' => 'shipping_state', + 'shipping_postcode' => 'shipping_postcode', + 'shipping_country' => 'shipping_country', + ); + + foreach ( $shipping_address_props as $meta_key => $prop ) { + $prop_key = substr( $prop, 9 ); + if ( ! isset( $changed_props['shipping'] ) || ! array_key_exists( $prop_key, $changed_props['shipping'] ) ) { + continue; + } + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props ); + } + + /** + * Gets the customers last order. + * + * @since 3.0.0 + * @param WC_Customer + * @return WC_Order|false + */ + public function get_last_order( &$customer ) { + global $wpdb; + + $last_order = $wpdb->get_var( "SELECT posts.ID + FROM $wpdb->posts AS posts + LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id + WHERE meta.meta_key = '_customer_user' + AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + ORDER BY posts.ID DESC + " ); + + if ( $last_order ) { + return wc_get_order( absint( $last_order ) ); + } else { + return false; + } + } + + /** + * Return the number of orders this customer has. + * + * @since 3.0.0 + * @param WC_Customer + * @return integer + */ + public function get_order_count( &$customer ) { + $count = get_user_meta( $customer->get_id(), '_order_count', true ); + + if ( '' === $count ) { + global $wpdb; + + $count = $wpdb->get_var( "SELECT COUNT(*) + FROM $wpdb->posts as posts + LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id + WHERE meta.meta_key = '_customer_user' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + AND meta_value = '" . esc_sql( $customer->get_id() ) . "' + " ); + update_user_meta( $customer->get_id(), '_order_count', $count ); + } + + return absint( $count ); + } + + /** + * Return how much money this customer has spent. + * + * @since 3.0.0 + * @param WC_Customer + * @return float + */ + public function get_total_spent( &$customer ) { + $spent = apply_filters( 'woocommerce_customer_get_total_spent', get_user_meta( $customer->get_id(), '_money_spent', true ), $customer ); + + if ( '' === $spent ) { + global $wpdb; + + $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + $spent = $wpdb->get_var( apply_filters( 'woocommerce_customer_get_total_spent_query', "SELECT SUM(meta2.meta_value) + FROM $wpdb->posts as posts + LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id + LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id + WHERE meta.meta_key = '_customer_user' + AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) + AND meta2.meta_key = '_order_total' + ", $customer ) ); + + if ( ! $spent ) { + $spent = 0; + } + update_user_meta( $customer->get_id(), '_money_spent', $spent ); + } + + return wc_format_decimal( $spent, 2 ); + } + + /** + * Search customers and return customer IDs. + * + * @param string $term + * @oaram int|string $limit @since 3.0.7 + * @return array + */ + public function search_customers( $term, $limit = '' ) { + $query = new WP_User_Query( array( + 'search' => '*' . esc_attr( $term ) . '*', + 'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ), + 'fields' => 'ID', + 'number' => $limit, + ) ); + + $query2 = new WP_User_Query( array( + 'fields' => 'ID', + 'number' => $limit, + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'first_name', + 'value' => $term, + 'compare' => 'LIKE', + ), + array( + 'key' => 'last_name', + 'value' => $term, + 'compare' => 'LIKE', + ), + ), + ) ); + + $results = wp_parse_id_list( array_merge( $query->get_results(), $query2->get_results() ) ); + + if ( $limit && count( $results ) > $limit ) { + $results = array_slice( $results, 0, $limit ); + } + + return $results; + } +} diff --git a/includes/data-stores/class-wc-customer-download-data-store.php b/includes/data-stores/class-wc-customer-download-data-store.php new file mode 100644 index 00000000000..f55fe4e2bc4 --- /dev/null +++ b/includes/data-stores/class-wc-customer-download-data-store.php @@ -0,0 +1,355 @@ +get_access_granted( 'edit' ) ) ) { + $download->set_access_granted( current_time( 'timestamp', true ) ); + } + + $data = array( + 'download_id' => $download->get_download_id( 'edit' ), + 'product_id' => $download->get_product_id( 'edit' ), + 'user_id' => $download->get_user_id( 'edit' ), + 'user_email' => $download->get_user_email( 'edit' ), + 'order_id' => $download->get_order_id( 'edit' ), + 'order_key' => $download->get_order_key( 'edit' ), + 'downloads_remaining' => $download->get_downloads_remaining( 'edit' ), + 'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), + 'download_count' => $download->get_download_count( 'edit' ), + 'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, + ); + + $format = array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%s', + ); + + $result = $wpdb->insert( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + apply_filters( 'woocommerce_downloadable_file_permission_data', $data ), + apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data ) + ); + + do_action( 'woocommerce_grant_product_download_access', $data ); + + if ( $result ) { + $download->set_id( $wpdb->insert_id ); + $download->apply_changes(); + } + } + + /** + * Method to read a download permission from the database. + * + * @param $download + * + * @throws Exception + */ + public function read( &$download ) { + global $wpdb; + + $download->set_defaults(); + + if ( ! $download->get_id() || ! ( $raw_download = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $download->get_id() ) ) ) ) { + throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); + } + + $download->set_props( array( + 'download_id' => $raw_download->download_id, + 'product_id' => $raw_download->product_id, + 'user_id' => $raw_download->user_id, + 'user_email' => $raw_download->user_email, + 'order_id' => $raw_download->order_id, + 'order_key' => $raw_download->order_key, + 'downloads_remaining' => $raw_download->downloads_remaining, + 'access_granted' => strtotime( $raw_download->access_granted ), + 'download_count' => $raw_download->download_count, + 'access_expires' => is_null( $raw_download->access_expires ) ? null : strtotime( $raw_download->access_expires ), + ) ); + $download->set_object_read( true ); + } + + /** + * Method to update a download in the database. + * + * @param WC_Customer_Download $download + */ + public function update( &$download ) { + global $wpdb; + + $data = array( + 'download_id' => $download->get_download_id( 'edit' ), + 'product_id' => $download->get_product_id( 'edit' ), + 'user_id' => $download->get_user_id( 'edit' ), + 'user_email' => $download->get_user_email( 'edit' ), + 'order_id' => $download->get_order_id( 'edit' ), + 'order_key' => $download->get_order_key( 'edit' ), + 'downloads_remaining' => $download->get_downloads_remaining( 'edit' ), + 'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), + 'download_count' => $download->get_download_count( 'edit' ), + 'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, + ); + + $format = array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%s', + ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + $data, + array( + 'permission_id' => $download->get_id(), + ), + $format + ); + $download->apply_changes(); + } + + /** + * Method to delete a download permission from the database. + * + * @param WC_Customer_Download $download + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$download, $args = array() ) { + global $wpdb; + + $wpdb->query( $wpdb->prepare( " + DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE permission_id = %d + ", $download->get_id() ) ); + + $download->set_id( 0 ); + } + + /** + * Method to delete a download permission from the database by ID. + * + * @param int $id + */ + public function delete_by_id( $id ) { + global $wpdb; + $wpdb->query( $wpdb->prepare( " + DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE permission_id = %d + ", $id ) ); + } + + /** + * Method to delete a download permission from the database by order ID. + * + * @param int $id + */ + public function delete_by_order_id( $id ) { + global $wpdb; + $wpdb->query( $wpdb->prepare( " + DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE order_id = %d + ", $id ) ); + } + + /** + * Method to delete a download permission from the database by download ID. + * + * @param int $id + */ + public function delete_by_download_id( $id ) { + global $wpdb; + $wpdb->query( $wpdb->prepare( " + DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE download_id = %s + ", $id ) ); + } + + /** + * Get a download object. + * + * @param array $data From the DB. + * @return WC_Customer_Download + */ + private function get_download( $data ) { + return new WC_Customer_Download( $data ); + } + + /** + * Get array of download ids by specified args. + * + * @param array $args + * @return array + */ + public function get_downloads( $args = array() ) { + global $wpdb; + + $args = wp_parse_args( $args, array( + 'user_email' => '', + 'order_id' => '', + 'order_key' => '', + 'product_id' => '', + 'download_id' => '', + 'orderby' => 'permission_id', + 'order' => 'DESC', + 'limit' => -1, + 'return' => 'objects', + ) ); + + $query = array(); + $query[] = "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE 1=1"; + + if ( $args['user_email'] ) { + $query[] = $wpdb->prepare( "AND user_email = %s", sanitize_email( $args['user_email'] ) ); + } + + if ( $args['order_id'] ) { + $query[] = $wpdb->prepare( "AND order_id = %d", $args['order_id'] ); + } + + if ( $args['order_key'] ) { + $query[] = $wpdb->prepare( "AND order_key = %s", $args['order_key'] ); + } + + if ( $args['product_id'] ) { + $query[] = $wpdb->prepare( "AND product_id = %d", $args['product_id'] ); + } + + if ( $args['download_id'] ) { + $query[] = $wpdb->prepare( "AND download_id = %s", $args['download_id'] ); + } + + $allowed_orders = array( 'permission_id', 'download_id', 'product_id', 'order_id', 'order_key', 'user_email', 'user_id', 'downloads_remaining', 'access_granted', 'access_expires', 'download_count' ); + $order = in_array( $args['order'], $allowed_orders ) ? $args['order'] : 'permission_id'; + $orderby = 'DESC' === strtoupper( $args['orderby'] ) ? 'DESC' : 'ASC'; + $orderby_sql = sanitize_sql_orderby( "{$order} {$orderby}" ); + $query[] = "ORDER BY {$orderby_sql}"; + + if ( 0 < $args['limit'] ) { + $query[] = $wpdb->prepare( "LIMIT %d", $args['limit'] ); + } + + $raw_downloads = $wpdb->get_results( implode( ' ', $query ) ); + + switch ( $args['return'] ) { + case 'ids' : + return wp_list_pluck( $raw_downloads, 'permission_id' ); + default : + return array_map( array( $this, 'get_download' ), $raw_downloads ); + } + } + + /** + * Update download ids if the hash changes. + * + * @param int $product_id + * @param string $old_id + * @param string $new_id + */ + public function update_download_id( $product_id, $old_id, $new_id ) { + global $wpdb; + + $wpdb->update( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + array( + 'download_id' => $new_id, + ), + array( + 'download_id' => $old_id, + 'product_id' => $product_id, + ) + ); + } + + /** + * Get a customers downloads. + * + * @param int $customer_id + * @return array + */ + public function get_downloads_for_customer( $customer_id ) { + global $wpdb; + + return $wpdb->get_results( + $wpdb->prepare( " + SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions + WHERE user_id = %d + AND permissions.order_id > 0 + AND + ( + permissions.downloads_remaining > 0 + OR permissions.downloads_remaining = '' + ) + AND + ( + permissions.access_expires IS NULL + OR permissions.access_expires >= %s + OR permissions.access_expires = '0000-00-00 00:00:00' + ) + ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id; + ", + $customer_id, + date( 'Y-m-d', current_time( 'timestamp' ) ) + ) + ); + } + + /** + * Update user prop for downloads based on order id. + * + * @param int $order_id + * @param int $customer_id + * @param string $email + */ + public function update_user_by_order_id( $order_id, $customer_id, $email ) { + global $wpdb; + $wpdb->update( $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + array( + 'user_id' => $customer_id, + 'user_email' => $email, + ), + array( + 'order_id' => $order_id, + ), + array( + '%d', + '%s', + ), + array( + '%d', + ) + ); + } +} diff --git a/includes/data-stores/class-wc-data-store-wp.php b/includes/data-stores/class-wc-data-store-wp.php new file mode 100644 index 00000000000..5eb53a85891 --- /dev/null +++ b/includes/data-stores/class-wc-data-store-wp.php @@ -0,0 +1,421 @@ +get_id(); + } + $terms = get_the_terms( $object_id, $taxonomy ); + if ( false === $terms || is_wp_error( $terms ) ) { + return array(); + } + return wp_list_pluck( $terms, 'term_id' ); + } + + /** + * Returns an array of meta for an object. + * + * @since 3.0.0 + * @param WC_Data + * @return array + */ + public function read_meta( &$object ) { + global $wpdb; + $db_info = $this->get_db_info(); + $raw_meta_data = $wpdb->get_results( $wpdb->prepare( " + SELECT {$db_info['meta_id_field']} as meta_id, meta_key, meta_value + FROM {$db_info['table']} + WHERE {$db_info['object_id_field']} = %d + ORDER BY {$db_info['meta_id_field']} + ", $object->get_id() ) ); + + $this->internal_meta_keys = array_merge( array_map( array( $this, 'prefix_key' ), $object->get_data_keys() ), $this->internal_meta_keys ); + return array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) ); + } + + /** + * Deletes meta based on meta ID. + * + * @since 3.0.0 + * @param WC_Data + * @param stdClass (containing at least ->id) + * @return array + */ + public function delete_meta( &$object, $meta ) { + delete_metadata_by_mid( $this->meta_type, $meta->id ); + } + + /** + * Add new piece of meta. + * + * @since 3.0.0 + * @param WC_Data + * @param stdClass (containing ->key and ->value) + * @return int meta ID + */ + public function add_meta( &$object, $meta ) { + return add_metadata( $this->meta_type, $object->get_id(), $meta->key, wp_slash( $meta->value ), false ); + } + + /** + * Update meta. + * + * @since 3.0.0 + * @param WC_Data + * @param stdClass (containing ->id, ->key and ->value) + */ + public function update_meta( &$object, $meta ) { + update_metadata_by_mid( $this->meta_type, $meta->id, $meta->value, $meta->key ); + } + + /** + * Table structure is slightly different between meta types, this function will return what we need to know. + * + * @since 3.0.0 + * @return array Array elements: table, object_id_field, meta_id_field + */ + protected function get_db_info() { + global $wpdb; + + $meta_id_field = 'meta_id'; // for some reason users calls this umeta_id so we need to track this as well. + $table = $wpdb->prefix; + + // If we are dealing with a type of metadata that is not a core type, the table should be prefixed. + if ( ! in_array( $this->meta_type, array( 'post', 'user', 'comment', 'term' ) ) ) { + $table .= 'woocommerce_'; + } + + $table .= $this->meta_type . 'meta'; + $object_id_field = $this->meta_type . '_id'; + + // Figure out our field names. + if ( 'user' === $this->meta_type ) { + $meta_id_field = 'umeta_id'; + $table = $wpdb->usermeta; + } + + if ( ! empty( $this->object_id_field_for_meta ) ) { + $object_id_field = $this->object_id_field_for_meta; + } + + return array( + 'table' => $table, + 'object_id_field' => $object_id_field, + 'meta_id_field' => $meta_id_field, + ); + } + + /** + * Internal meta keys we don't want exposed as part of meta_data. This is in + * addition to all data props with _ prefix. + * @since 2.6.0 + * + * @param string $key + * + * @return string + */ + protected function prefix_key( $key ) { + return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key; + } + + /** + * Callback to remove unwanted meta data. + * + * @param object $meta + * @return bool + */ + protected function exclude_internal_meta_keys( $meta ) { + return ! in_array( $meta->meta_key, $this->internal_meta_keys ) && 0 !== stripos( $meta->meta_key, 'wp_' ); + } + + /** + * Gets a list of props and meta keys that need updated based on change state + * or if they are present in the database or not. + * + * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc). + * @param array $meta_key_to_props A mapping of meta keys => prop names. + * @param string $meta_type The internal WP meta type (post, user, etc). + * @return array A mapping of meta keys => prop names, filtered by ones that should be updated. + */ + protected function get_props_to_update( $object, $meta_key_to_props, $meta_type = 'post' ) { + $props_to_update = array(); + $changed_props = $object->get_changes(); + + // Props should be updated if they are a part of the $changed array or don't exist yet. + foreach ( $meta_key_to_props as $meta_key => $prop ) { + if ( array_key_exists( $prop, $changed_props ) || ! metadata_exists( $meta_type, $object->get_id(), $meta_key ) ) { + $props_to_update[ $meta_key ] = $prop; + } + } + + return $props_to_update; + } + + /** + * Get valid WP_Query args from a WC_Object_Query's query variables. + * + * @since 3.1.0 + * @param array $query_vars query vars from a WC_Object_Query + * @return array + */ + protected function get_wp_query_args( $query_vars ) { + + $skipped_values = array( '', array(), null ); + $wp_query_args = array( + 'errors' => array(), + 'meta_query' => array(), + ); + + foreach ( $query_vars as $key => $value ) { + if ( in_array( $value, $skipped_values, true ) || 'meta_query' === $key ) { + continue; + } + + // Build meta queries out of vars that are stored in internal meta keys. + if ( in_array( '_' . $key, $this->internal_meta_keys ) ) { + $wp_query_args['meta_query'][] = array( + 'key' => '_' . $key, + 'value' => $value, + 'compare' => '=', + ); + // Other vars get mapped to wp_query args or just left alone. + } else { + $key_mapping = array( + 'parent' => 'post_parent', + 'parent_exclude' => 'post_parent__not_in', + 'exclude' => 'post__not_in', + 'limit' => 'posts_per_page', + 'type' => 'post_type', + 'return' => 'fields', + ); + + if ( isset( $key_mapping[ $key ] ) ) { + $wp_query_args[ $key_mapping[ $key ] ] = $value; + } else { + $wp_query_args[ $key ] = $value; + } + } + } + + return apply_filters( 'woocommerce_get_wp_query_args', $wp_query_args, $query_vars ); + } + + /** + * Map a valid date query var to WP_Query arguments. + * Valid date formats: YYYY-MM-DD or timestamp, possibly combined with an operator from $valid_operators. + * Also accepts a WC_DateTime object. + * @param mixed $query_var A valid date format + * @param string $key meta or db column key + * @param array $wp_query_args WP_Query args + * @return array Modified $wp_query_args + */ + protected function parse_date_for_wp_query( $query_var, $key, $wp_query_args = array() ) { + $query_parse_regex = '/([^.<>]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/'; + $valid_operators = array( '>', '>=', '=', '<=', '<', '...' ); + + // YYYY-MM-DD queries have 'day' precision. Timestamp/WC_DateTime queries have 'second' precision. + $precision = 'second'; + + $dates = array(); + $operator = '='; + + try { + // Specific time query with a WC_DateTime. + if ( is_a( $query_var, 'WC_DateTime' ) ) { + $dates[] = $query_var; + + // Specific time query with a timestamp. + } elseif ( is_numeric( $query_var ) ) { + $dates[] = new WC_DateTime( "@{$query_var}", new DateTimeZone( 'UTC' ) ); + + // Query with operators and possible range of dates. + } elseif ( preg_match( $query_parse_regex, $query_var, $sections ) ) { + if ( ! empty( $sections[1] ) ) { + $dates[] = is_numeric( $sections[1] ) ? new WC_DateTime( "@{$sections[1]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[1] ); + } + + $operator = in_array( $sections[2], $valid_operators ) ? $sections[2] : ''; + $dates[] = is_numeric( $sections[3] ) ? new WC_DateTime( "@{$sections[3]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[3] ); + + if ( ! is_numeric( $sections[1] ) && ! is_numeric( $sections[3] ) ) { + $precision = 'day'; + } + + // Specific time query with a string. + } else { + $dates[] = wc_string_to_datetime( $query_var ); + $precision = 'day'; + } + } catch ( Exception $e ) { + return $wp_query_args; + } + + // Check for valid inputs. + if ( ! $operator || empty( $dates ) || ( '...' === $operator && count( $dates ) < 2 ) ) { + return $wp_query_args; + } + + // Build date query for 'post_date' or 'post_modified' keys. + if ( 'post_date' === $key || 'post_modified' === $key ) { + if ( ! isset( $wp_query_args['date_query'] ) ) { + $wp_query_args['date_query'] = array(); + } + + $query_arg = array( + 'column' => 'day' === $precision ? $key : $key . '_gmt', + 'inclusive' => '>' !== $operator && '<' !== $operator, + ); + + // Add 'before'/'after' query args. + $comparisons = array(); + if ( '>' === $operator || '>=' === $operator || '...' === $operator ) { + $comparisons[] = 'after'; + } + if ( '<' === $operator || '<=' === $operator || '...' === $operator ) { + $comparisons[] = 'before'; + } + + foreach ( $comparisons as $index => $comparison ) { + /** + * WordPress doesn't generate the correct SQL for inclusive day queries with both a 'before' and + * 'after' string query, so we have to use the array format in 'day' precision. + * @see https://core.trac.wordpress.org/ticket/29908 + */ + if ( 'day' === $precision ) { + $query_arg[ $comparison ]['year'] = $dates[ $index ]->date( 'Y' ); + $query_arg[ $comparison ]['month'] = $dates[ $index ]->date( 'n' ); + $query_arg[ $comparison ]['day'] = $dates[ $index ]->date( 'j' ); + /** + * WordPress doesn't support 'hour'/'second'/'minute' in array format 'before'/'after' queries, + * so we have to use a string query. + */ + } else { + $query_arg[ $comparison ] = gmdate( 'm/d/Y H:i:s', $dates[ $index ]->getTimestamp() ); + } + } + + if ( empty( $comparisons ) ) { + $query_arg['year'] = $dates[0]->date( 'Y' ); + $query_arg['month'] = $dates[0]->date( 'n' ); + $query_arg['day'] = $dates[0]->date( 'j' ); + if ( 'second' === $precision ) { + $query_arg['hour'] = $dates[0]->date( 'H' ); + $query_arg['minute'] = $dates[0]->date( 'i' ); + $query_arg['second'] = $dates[0]->date( 's' ); + } + } + $wp_query_args['date_query'][] = $query_arg; + return $wp_query_args; + } + + // Build meta query for unrecognized keys. + if ( ! isset( $wp_query_args['meta_query'] ) ) { + $wp_query_args['meta_query'] = array(); + } + + // Meta dates are stored as timestamps in the db. + // Check against begining/end-of-day timestamps when using 'day' precision. + if ( 'day' === $precision ) { + $start_timestamp = strtotime( gmdate( 'm/d/Y 00:00:00', $dates[0]->getTimestamp() ) ); + $end_timestamp = '...' !== $operator ? ( $start_timestamp + DAY_IN_SECONDS ) : strtotime( gmdate( 'm/d/Y 00:00:00', $dates[1]->getTimestamp() ) ); + switch ( $operator ) { + case '>': + case '<=': + $wp_query_args['meta_query'][] = array( + 'key' => $key, + 'value' => $end_timestamp, + 'compare' => $operator, + ); + break; + + case '<': + case '>=': + $wp_query_args['meta_query'][] = array( + 'key' => $key, + 'value' => $start_timestamp, + 'compare' => $operator, + ); + break; + + default: + $wp_query_args['meta_query'][] = array( + 'key' => $key, + 'value' => $start_timestamp, + 'compare' => '>=', + ); + $wp_query_args['meta_query'][] = array( + 'key' => $key, + 'value' => $end_timestamp, + 'compare' => '<=', + ); + } + } else { + if ( '...' !== $operator ) { + $wp_query_args['meta_query'][] = array( + 'key' => $key, + 'value' => $dates[0]->getTimestamp(), + 'compare' => $operator, + ); + } else { + $wp_query_args['meta_query'][] = array( + 'key' => $key, + 'value' => $dates[0]->getTimestamp(), + 'compare' => '>=', + ); + $wp_query_args['meta_query'][] = array( + 'key' => $key, + 'value' => $dates[1]->getTimestamp(), + 'compare' => '<=', + ); + } + } + + return $wp_query_args; + } +} diff --git a/includes/data-stores/class-wc-order-data-store-cpt.php b/includes/data-stores/class-wc-order-data-store-cpt.php new file mode 100644 index 00000000000..a6dd29170d6 --- /dev/null +++ b/includes/data-stores/class-wc-order-data-store-cpt.php @@ -0,0 +1,706 @@ +set_order_key( 'wc_' . apply_filters( 'woocommerce_generate_order_key', uniqid( 'order_' ) ) ); + parent::create( $order ); + do_action( 'woocommerce_new_order', $order->get_id() ); + } + + /** + * Read order data. Can be overridden by child classes to load other props. + * + * @param WC_Order + * @param object $post_object + * @since 3.0.0 + */ + protected function read_order_data( &$order, $post_object ) { + parent::read_order_data( $order, $post_object ); + $id = $order->get_id(); + $date_completed = get_post_meta( $id, '_date_completed', true ); + $date_paid = get_post_meta( $id, '_date_paid', true ); + + if ( ! $date_completed ) { + $date_completed = get_post_meta( $id, '_completed_date', true ); + } + + if ( ! $date_paid ) { + $date_paid = get_post_meta( $id, '_paid_date', true ); + } + + $order->set_props( array( + 'order_key' => get_post_meta( $id, '_order_key', true ), + 'customer_id' => get_post_meta( $id, '_customer_user', true ), + 'billing_first_name' => get_post_meta( $id, '_billing_first_name', true ), + 'billing_last_name' => get_post_meta( $id, '_billing_last_name', true ), + 'billing_company' => get_post_meta( $id, '_billing_company', true ), + 'billing_address_1' => get_post_meta( $id, '_billing_address_1', true ), + 'billing_address_2' => get_post_meta( $id, '_billing_address_2', true ), + 'billing_city' => get_post_meta( $id, '_billing_city', true ), + 'billing_state' => get_post_meta( $id, '_billing_state', true ), + 'billing_postcode' => get_post_meta( $id, '_billing_postcode', true ), + 'billing_country' => get_post_meta( $id, '_billing_country', true ), + 'billing_email' => get_post_meta( $id, '_billing_email', true ), + 'billing_phone' => get_post_meta( $id, '_billing_phone', true ), + 'shipping_first_name' => get_post_meta( $id, '_shipping_first_name', true ), + 'shipping_last_name' => get_post_meta( $id, '_shipping_last_name', true ), + 'shipping_company' => get_post_meta( $id, '_shipping_company', true ), + 'shipping_address_1' => get_post_meta( $id, '_shipping_address_1', true ), + 'shipping_address_2' => get_post_meta( $id, '_shipping_address_2', true ), + 'shipping_city' => get_post_meta( $id, '_shipping_city', true ), + 'shipping_state' => get_post_meta( $id, '_shipping_state', true ), + 'shipping_postcode' => get_post_meta( $id, '_shipping_postcode', true ), + 'shipping_country' => get_post_meta( $id, '_shipping_country', true ), + 'payment_method' => get_post_meta( $id, '_payment_method', true ), + 'payment_method_title' => get_post_meta( $id, '_payment_method_title', true ), + 'transaction_id' => get_post_meta( $id, '_transaction_id', true ), + 'customer_ip_address' => get_post_meta( $id, '_customer_ip_address', true ), + 'customer_user_agent' => get_post_meta( $id, '_customer_user_agent', true ), + 'created_via' => get_post_meta( $id, '_created_via', true ), + 'date_completed' => $date_completed, + 'date_paid' => $date_paid, + 'cart_hash' => get_post_meta( $id, '_cart_hash', true ), + 'customer_note' => $post_object->post_excerpt, + ) ); + } + + /** + * Method to update an order in the database. + * @param WC_Order $order + */ + public function update( &$order ) { + // Before updating, ensure date paid is set if missing. + if ( ! $order->get_date_paid( 'edit' ) && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) ) { + $order->set_date_paid( $order->get_date_created( 'edit' ) ); + } + + // Update the order. + parent::update( $order ); + + do_action( 'woocommerce_update_order', $order->get_id() ); + } + + /** + * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. + * + * @param WC_Order + * @since 3.0.0 + */ + protected function update_post_meta( &$order ) { + $updated_props = array(); + $id = $order->get_id(); + $meta_key_to_props = array( + '_order_key' => 'order_key', + '_customer_user' => 'customer_id', + '_payment_method' => 'payment_method', + '_payment_method_title' => 'payment_method_title', + '_transaction_id' => 'transaction_id', + '_customer_ip_address' => 'customer_ip_address', + '_customer_user_agent' => 'customer_user_agent', + '_created_via' => 'created_via', + '_date_completed' => 'date_completed', + '_date_paid' => 'date_paid', + '_cart_hash' => 'cart_hash', + ); + + $props_to_update = $this->get_props_to_update( $order, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + + if ( 'date_paid' === $prop ) { + // In 3.0.x we store this as a UTC timestamp. + update_post_meta( $id, $meta_key, ! is_null( $value ) ? $value->getTimestamp() : '' ); + + // In 2.6.x date_paid was stored as _paid_date in local mysql format. + update_post_meta( $id, '_paid_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); + + } elseif ( 'date_completed' === $prop ) { + // In 3.0.x we store this as a UTC timestamp. + update_post_meta( $id, $meta_key, ! is_null( $value ) ? $value->getTimestamp() : '' ); + + // In 2.6.x date_paid was stored as _paid_date in local mysql format. + update_post_meta( $id, '_completed_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); + + } else { + update_post_meta( $id, $meta_key, $value ); + } + + $updated_props[] = $prop; + } + + $address_props = array( + 'billing' => array( + '_billing_first_name' => 'billing_first_name', + '_billing_last_name' => 'billing_last_name', + '_billing_company' => 'billing_company', + '_billing_address_1' => 'billing_address_1', + '_billing_address_2' => 'billing_address_2', + '_billing_city' => 'billing_city', + '_billing_state' => 'billing_state', + '_billing_postcode' => 'billing_postcode', + '_billing_country' => 'billing_country', + '_billing_email' => 'billing_email', + '_billing_phone' => 'billing_phone', + ), + 'shipping' => array( + '_shipping_first_name' => 'shipping_first_name', + '_shipping_last_name' => 'shipping_last_name', + '_shipping_company' => 'shipping_company', + '_shipping_address_1' => 'shipping_address_1', + '_shipping_address_2' => 'shipping_address_2', + '_shipping_city' => 'shipping_city', + '_shipping_state' => 'shipping_state', + '_shipping_postcode' => 'shipping_postcode', + '_shipping_country' => 'shipping_country', + ), + ); + + foreach ( $address_props as $props_key => $props ) { + $props_to_update = $this->get_props_to_update( $order, $props ); + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + update_post_meta( $id, $meta_key, $value ); + $updated_props[] = $prop; + $updated_props[] = $props_key; + } + } + + parent::update_post_meta( $order ); + + // If address changed, store concatenated version to make searches faster. + if ( in_array( 'billing', $updated_props ) || ! metadata_exists( 'post', $id, '_billing_address_index' ) ) { + update_post_meta( $id, '_billing_address_index', implode( ' ', $order->get_address( 'billing' ) ) ); + } + if ( in_array( 'shipping', $updated_props ) || ! metadata_exists( 'post', $id, '_shipping_address_index' ) ) { + update_post_meta( $id, '_shipping_address_index', implode( ' ', $order->get_address( 'shipping' ) ) ); + } + + // If customer changed, update any downloadable permissions. + if ( in_array( 'customer_user', $updated_props ) || in_array( 'billing_email', $updated_props ) ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->update_user_by_order_id( $id, $order->get_customer_id(), $order->get_billing_email() ); + } + + do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); + } + + /** + * Excerpt for post. + * + * @param WC_Order $order + * @return string + */ + protected function get_post_excerpt( $order ) { + return $order->get_customer_note(); + } + + /** + * Get amount already refunded. + * + * @param WC_Order + * @return string + */ + public function get_total_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( $wpdb->prepare( " + SELECT SUM( postmeta.meta_value ) + FROM $wpdb->postmeta AS postmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + WHERE postmeta.meta_key = '_refund_amount' + AND postmeta.post_id = posts.ID + ", $order->get_id() ) ); + + return $total; + } + + /** + * Get the total tax refunded. + * + * @param WC_Order + * @return float + */ + public function get_total_tax_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( $wpdb->prepare( " + SELECT SUM( order_itemmeta.meta_value ) + FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' ) + WHERE order_itemmeta.order_item_id = order_items.order_item_id + AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount') + ", $order->get_id() ) ); + + return abs( $total ); + } + + /** + * Get the total shipping refunded. + * + * @param WC_Order + * @return float + */ + public function get_total_shipping_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( $wpdb->prepare( " + SELECT SUM( order_itemmeta.meta_value ) + FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' ) + WHERE order_itemmeta.order_item_id = order_items.order_item_id + AND order_itemmeta.meta_key IN ('cost') + ", $order->get_id() ) ); + + return abs( $total ); + } + + /** + * Finds an Order ID based on an order key. + * + * @param string $order_key An order key has generated by + * @return int The ID of an order, or 0 if the order could not be found + */ + public function get_order_id_by_order_key( $order_key ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_order_key' AND meta_value = %s", $order_key ) ); + } + + /** + * Return count of orders with a specific status. + * + * @param string $status + * @return int + */ + public function get_order_count( $status ) { + global $wpdb; + return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", $status ) ) ); + } + + /** + * Get all orders matching the passed in args. + * + * @deprecated 3.1.0 - Use wc_get_orders instead. + * @see wc_get_orders() + * + * @param array $args + * + * @return array|object + */ + public function get_orders( $args = array() ) { + wc_deprecated_function( 'WC_Order_Data_Store_CPT::get_orders', '3.1.0', 'Use wc_get_orders instead.' ); + return wc_get_orders( $args ); + } + + /** + * Generate meta query for wc_get_orders. + * + * @param array $values + * @param string $relation + * @return array + */ + private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) { + $meta_query = array( + 'relation' => strtoupper( $relation ), + 'customer_emails' => array( + 'key' => '_billing_email', + 'value' => array(), + 'compare' => 'IN', + ), + 'customer_ids' => array( + 'key' => '_customer_user', + 'value' => array(), + 'compare' => 'IN', + ), + ); + foreach ( $values as $value ) { + if ( is_array( $value ) ) { + $query_part = $this->get_orders_generate_customer_meta_query( $value, 'and' ); + if ( is_wp_error( $query_part ) ) { + return $query_part; + } + $meta_query[] = $query_part; + } elseif ( is_email( $value ) ) { + $meta_query['customer_emails']['value'][] = sanitize_email( $value ); + } elseif ( is_numeric( $value ) ) { + $meta_query['customer_ids']['value'][] = strval( absint( $value ) ); + } else { + return new WP_Error( 'woocommerce_query_invalid', __( 'Invalid customer query.', 'woocommerce' ), $values ); + } + } + + if ( empty( $meta_query['customer_emails']['value'] ) ) { + unset( $meta_query['customer_emails'] ); + unset( $meta_query['relation'] ); + } + + if ( empty( $meta_query['customer_ids']['value'] ) ) { + unset( $meta_query['customer_ids'] ); + unset( $meta_query['relation'] ); + } + + return $meta_query; + } + + /** + * Get unpaid orders after a certain date, + * + * @param int $date timestamp + * @return array + */ + public function get_unpaid_orders( $date ) { + global $wpdb; + + $unpaid_orders = $wpdb->get_col( $wpdb->prepare( " + SELECT posts.ID + FROM {$wpdb->posts} AS posts + WHERE posts.post_type IN ('" . implode( "','", wc_get_order_types() ) . "') + AND posts.post_status = 'wc-pending' + AND posts.post_modified < %s + ", date( 'Y-m-d H:i:s', absint( $date ) ) ) ); + + return $unpaid_orders; + } + + /** + * Search order data for a term and return ids. + * + * @param string $term + * @return array of ids + */ + public function search_orders( $term ) { + global $wpdb; + + /** + * Searches on meta data can be slow - this lets you choose what fields to search. + * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster. + * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed). + * @var array + */ + $search_fields = array_map( 'wc_clean', apply_filters( 'woocommerce_shop_order_search_fields', array( + '_billing_address_index', + '_shipping_address_index', + '_billing_last_name', + '_billing_email', + ) ) ); + $order_ids = array(); + + if ( is_numeric( $term ) ) { + $order_ids[] = absint( $term ); + } + + if ( ! empty( $search_fields ) ) { + $order_ids = array_unique( array_merge( + $order_ids, + $wpdb->get_col( + $wpdb->prepare( "SELECT DISTINCT p1.post_id FROM {$wpdb->postmeta} p1 WHERE p1.meta_value LIKE '%%%s%%'", $wpdb->esc_like( wc_clean( $term ) ) ) . " AND p1.meta_key IN ('" . implode( "','", array_map( 'esc_sql', $search_fields ) ) . "')" + ), + $wpdb->get_col( + $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items as order_items + WHERE order_item_name LIKE '%%%s%%' + ", + $wpdb->esc_like( wc_clean( $term ) ) + ) + ) + ) ); + } + + return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields ); + } + + /** + * Gets information about whether permissions were generated yet. + * + * @param WC_Order|int $order + * @return bool + */ + public function get_download_permissions_granted( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) ); + } + + /** + * Stores information about whether permissions were generated yet. + * + * @param WC_Order|int $order + * @param bool $set + */ + public function set_download_permissions_granted( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) ); + } + + /** + * Gets information about whether sales were recorded. + * + * @param WC_Order|int $order + * @return bool + */ + public function get_recorded_sales( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) ); + } + + /** + * Stores information about whether sales were recorded. + * + * @param WC_Order|int $order + * @param bool $set + */ + public function set_recorded_sales( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) ); + } + + /** + * Gets information about whether coupon counts were updated. + * + * @param WC_Order|int $order + * @return bool + */ + public function get_recorded_coupon_usage_counts( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) ); + } + + /** + * Stores information about whether coupon counts were updated. + * + * @param WC_Order|int $order + * @param bool $set + */ + public function set_recorded_coupon_usage_counts( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) ); + } + + /** + * Gets information about whether stock was reduced. + * + * @param WC_Order|int $order + * @return bool + */ + public function get_stock_reduced( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) ); + } + + /** + * Stores information about whether stock was reduced. + * + * @param WC_Order|int $order + * @param bool $set + */ + public function set_stock_reduced( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) ); + } + + /** + * Get the order type based on Order ID. + * + * @since 3.0.0 + * @param int $order_id + * @return string + */ + public function get_order_type( $order_id ) { + return get_post_type( $order_id ); + } + + /** + * Get valid WP_Query args from a WC_Order_Query's query variables. + * + * @since 3.1.0 + * @param array $query_vars query vars from a WC_Order_Query + * @return array + */ + protected function get_wp_query_args( $query_vars ) { + + // Map query vars to ones that get_wp_query_args or WP_Query recognize. + $key_mapping = array( + 'customer_id' => 'customer_user', + 'status' => 'post_status', + 'currency' => 'order_currency', + 'version' => 'order_version', + 'discount_total' => 'cart_discount', + 'discount_tax' => 'cart_discount_tax', + 'shipping_total' => 'order_shipping', + 'shipping_tax' => 'order_shipping_tax', + 'cart_tax' => 'order_tax', + 'total' => 'order_total', + ); + + foreach ( $key_mapping as $query_key => $db_key ) { + if ( isset( $query_vars[ $query_key ] ) ) { + $query_vars[ $db_key ] = $query_vars[ $query_key ]; + unset( $query_vars[ $query_key ] ); + } + } + + // Add the 'wc-' prefix to status if needed. + if ( ! empty( $query_vars['post_status'] ) ) { + if ( is_array( $query_vars['post_status'] ) ) { + foreach ( $query_vars['post_status'] as &$status ) { + $status = wc_is_order_status( 'wc-' . $status ) ? 'wc-' . $status : $status; + } + } else { + $query_vars['post_status'] = wc_is_order_status( 'wc-' . $query_vars['post_status'] ) ? 'wc-' . $query_vars['post_status'] : $query_vars['post_status']; + } + } + + $wp_query_args = parent::get_wp_query_args( $query_vars ); + + if ( ! isset( $wp_query_args['date_query'] ) ) { + $wp_query_args['date_query'] = array(); + } + if ( ! isset( $wp_query_args['meta_query'] ) ) { + $wp_query_args['meta_query'] = array(); + } + + $date_queries = array( + 'date_created' => 'post_date', + 'date_modified' => 'post_modified', + 'date_completed' => '_date_completed', + 'date_paid' => '_date_paid', + ); + foreach ( $date_queries as $query_var_key => $db_key ) { + if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { + + // Remove any existing meta queries for the same keys to prevent conflicts. + $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); + foreach ( $existing_queries as $query_index => $query_contents ) { + unset( $wp_query_args['meta_query'][ $query_index ] ); + } + + $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); + } + } + + if ( isset( $query_vars['customer'] ) && '' !== $query_vars['customer'] && array() !== $query_vars['customer'] ) { + $values = is_array( $query_vars['customer'] ) ? $query_vars['customer'] : array( $query_vars['customer'] ); + $customer_query = $this->get_orders_generate_customer_meta_query( $values ); + if ( is_wp_error( $customer_query ) ) { + $wp_query_args['errors'][] = $customer_query; + } else { + $wp_query_args['meta_query'][] = $customer_query; + } + } + + if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { + $wp_query_args['no_found_rows'] = true; + } + + return apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $query_vars, $this ); + } + + /** + * Query for Orders matching specific criteria. + * + * @since 3.1.0 + * + * @param array $query_vars query vars from a WC_Order_Query + * + * @return array|object + */ + public function query( $query_vars ) { + $args = $this->get_wp_query_args( $query_vars ); + + if ( ! empty( $args['errors'] ) ) { + $query = (object) array( + 'posts' => array(), + 'found_posts' => 0, + 'max_num_pages' => 0, + ); + } else { + $query = new WP_Query( $args ); + } + + $orders = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_order', $query->posts ) ); + + if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { + return (object) array( + 'orders' => $orders, + 'total' => $query->found_posts, + 'max_num_pages' => $query->max_num_pages, + ); + } + + return $orders; + } +} diff --git a/includes/data-stores/class-wc-order-item-coupon-data-store.php b/includes/data-stores/class-wc-order-item-coupon-data-store.php new file mode 100644 index 00000000000..ac2780d20ce --- /dev/null +++ b/includes/data-stores/class-wc-order-item-coupon-data-store.php @@ -0,0 +1,55 @@ +get_id(); + $item->set_props( array( + 'discount' => get_metadata( 'order_item', $id, 'discount_amount', true ), + 'discount_tax' => get_metadata( 'order_item', $id, 'discount_amount_tax', true ), + ) ); + $item->set_object_read( true ); + } + + /** + * Saves an item's data to the database / item meta. + * Ran after both create and update, so $item->get_id() will be set. + * + * @since 3.0.0 + * @param WC_Order_Item_Coupon $item + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $save_values = array( + 'discount_amount' => $item->get_discount( 'edit' ), + 'discount_amount_tax' => $item->get_discount_tax( 'edit' ), + ); + foreach ( $save_values as $key => $value ) { + update_metadata( 'order_item', $id, $key, $value ); + } + } +} diff --git a/includes/data-stores/class-wc-order-item-data-store.php b/includes/data-stores/class-wc-order-item-data-store.php new file mode 100644 index 00000000000..00d8b234846 --- /dev/null +++ b/includes/data-stores/class-wc-order-item-data-store.php @@ -0,0 +1,149 @@ +insert( + $wpdb->prefix . "woocommerce_order_items", + array( + 'order_item_name' => $item['order_item_name'], + 'order_item_type' => $item['order_item_type'], + 'order_id' => $order_id, + ), + array( + '%s', + '%s', + '%d', + ) + ); + + return absint( $wpdb->insert_id ); + } + + /** + * Update an order item. + * + * @since 3.0.0 + * @param int $item_id + * @param array $item order_item_name or order_item_type. + * @return boolean + */ + public function update_order_item( $item_id, $item ) { + global $wpdb; + return $wpdb->update( $wpdb->prefix . 'woocommerce_order_items', $item, array( 'order_item_id' => $item_id ) ); + } + + /** + * Delete an order item. + * + * @since 3.0.0 + * @param int $item_id + */ + public function delete_order_item( $item_id ) { + global $wpdb; + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d", $item_id ) ); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d", $item_id ) ); + } + + /** + * Update term meta. + * + * @since 3.0.0 + * @param int $item_id + * @param string $meta_key + * @param mixed $meta_value + * @param string $prev_value (default: '') + * @return bool + */ + public function update_metadata( $item_id, $meta_key, $meta_value, $prev_value = '' ) { + return update_metadata( 'order_item', $item_id, $meta_key, wp_slash( $meta_value ), $prev_value ); + } + + /** + * Add term meta. + * + * @since 3.0.0 + * @param int $item_id + * @param string $meta_key + * @param mixed $meta_value + * @param bool $unique (default: false) + * @return int New row ID or 0 + */ + public function add_metadata( $item_id, $meta_key, $meta_value, $unique = false ) { + return add_metadata( 'order_item', $item_id, $meta_key, wp_slash( $meta_value ), $unique ); + } + + /** + * Delete term meta. + * + * @since 3.0.0 + * @param int $item_id + * @param string $meta_key + * @param string $meta_value (default: '') + * @param bool $delete_all (default: false) + * @return bool + */ + public function delete_metadata( $item_id, $meta_key, $meta_value = '', $delete_all = false ) { + return delete_metadata( 'order_item', $item_id, $meta_key, wp_slash( $meta_value ), $delete_all ); + } + + /** + * Get term meta. + * + * @since 3.0.0 + * @param int $item_id + * @param string $key + * @param bool $single (default: true) + * @return mixed + */ + public function get_metadata( $item_id, $key, $single = true ) { + return get_metadata( 'order_item', $item_id, $key, $single ); + } + + /** + * Get order ID by order item ID. + * + * @since 3.0.0 + * @param int $item_id + * @return int + */ + function get_order_id_by_order_item_id( $item_id ) { + global $wpdb; + return (int) $wpdb->get_var( $wpdb->prepare( + "SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d", + $item_id + ) ); + } + + /** + * Get the order item type based on Item ID. + * + * @since 3.0.0 + * @param int $item_id + * @return string + */ + public function get_order_item_type( $item_id ) { + global $wpdb; + $item_data = $wpdb->get_row( $wpdb->prepare( "SELECT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;", $item_id ) ); + return $item_data->order_item_type; + } +} diff --git a/includes/data-stores/class-wc-order-item-fee-data-store.php b/includes/data-stores/class-wc-order-item-fee-data-store.php new file mode 100644 index 00000000000..6f748dde876 --- /dev/null +++ b/includes/data-stores/class-wc-order-item-fee-data-store.php @@ -0,0 +1,60 @@ +get_id(); + $item->set_props( array( + 'tax_class' => get_metadata( 'order_item', $id, '_tax_class', true ), + 'tax_status' => get_metadata( 'order_item', $id, '_tax_status', true ), + 'total' => get_metadata( 'order_item', $id, '_line_total', true ), + 'taxes' => get_metadata( 'order_item', $id, '_line_tax_data', true ), + ) ); + $item->set_object_read( true ); + } + + /** + * Saves an item's data to the database / item meta. + * Ran after both create and update, so $id will be set. + * + * @since 3.0.0 + * @param WC_Order_Item_Fee $item + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $save_values = array( + '_tax_class' => $item->get_tax_class( 'edit' ), + '_tax_status' => $item->get_tax_status( 'edit' ), + '_line_total' => $item->get_total( 'edit' ), + '_line_tax' => $item->get_total_tax( 'edit' ), + '_line_tax_data' => $item->get_taxes( 'edit' ), + ); + foreach ( $save_values as $key => $value ) { + update_metadata( 'order_item', $id, $key, $value ); + } + } +} diff --git a/includes/data-stores/class-wc-order-item-product-store.php b/includes/data-stores/class-wc-order-item-product-store.php new file mode 100644 index 00000000000..f7f8025b640 --- /dev/null +++ b/includes/data-stores/class-wc-order-item-product-store.php @@ -0,0 +1,87 @@ +get_id(); + $item->set_props( array( + 'product_id' => get_metadata( 'order_item', $id, '_product_id', true ), + 'variation_id' => get_metadata( 'order_item', $id, '_variation_id', true ), + 'quantity' => get_metadata( 'order_item', $id, '_qty', true ), + 'tax_class' => get_metadata( 'order_item', $id, '_tax_class', true ), + 'subtotal' => get_metadata( 'order_item', $id, '_line_subtotal', true ), + 'total' => get_metadata( 'order_item', $id, '_line_total', true ), + 'taxes' => get_metadata( 'order_item', $id, '_line_tax_data', true ), + ) ); + $item->set_object_read( true ); + } + + /** + * Saves an item's data to the database / item meta. + * Ran after both create and update, so $id will be set. + * + * @since 3.0.0 + * @param WC_Order_Item_Product $item + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $save_values = array( + '_product_id' => $item->get_product_id( 'edit' ), + '_variation_id' => $item->get_variation_id( 'edit' ), + '_qty' => $item->get_quantity( 'edit' ), + '_tax_class' => $item->get_tax_class( 'edit' ), + '_line_subtotal' => $item->get_subtotal( 'edit' ), + '_line_subtotal_tax' => $item->get_subtotal_tax( 'edit' ), + '_line_total' => $item->get_total( 'edit' ), + '_line_tax' => $item->get_total_tax( 'edit' ), + '_line_tax_data' => $item->get_taxes( 'edit' ), + ); + foreach ( $save_values as $key => $value ) { + update_metadata( 'order_item', $id, $key, $value ); + } + } + + /** + * Get a list of download IDs for a specific item from an order. + * + * @since 3.0.0 + * @param WC_Order_Item_Product $item + * @param WC_Order $order + * @return array + */ + public function get_download_ids( $item, $order ) { + global $wpdb; + return $wpdb->get_col( + $wpdb->prepare( + "SELECT download_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_email = %s AND order_key = %s AND product_id = %d ORDER BY permission_id", + $order->get_billing_email(), + $order->get_order_key(), + $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id() + ) + ); + } +} diff --git a/includes/data-stores/class-wc-order-item-shipping-data-store.php b/includes/data-stores/class-wc-order-item-shipping-data-store.php new file mode 100644 index 00000000000..48cd5b8c53d --- /dev/null +++ b/includes/data-stores/class-wc-order-item-shipping-data-store.php @@ -0,0 +1,58 @@ +get_id(); + $item->set_props( array( + 'method_id' => get_metadata( 'order_item', $id, 'method_id', true ), + 'total' => get_metadata( 'order_item', $id, 'cost', true ), + 'taxes' => get_metadata( 'order_item', $id, 'taxes', true ), + ) ); + $item->set_object_read( true ); + } + + /** + * Saves an item's data to the database / item meta. + * Ran after both create and update, so $id will be set. + * + * @since 3.0.0 + * @param WC_Order_Item_Shipping $item + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $save_values = array( + 'method_id' => $item->get_method_id( 'edit' ), + 'cost' => $item->get_total( 'edit' ), + 'total_tax' => $item->get_total_tax( 'edit' ), + 'taxes' => $item->get_taxes( 'edit' ), + ); + foreach ( $save_values as $key => $value ) { + update_metadata( 'order_item', $id, $key, $value ); + } + } +} diff --git a/includes/data-stores/class-wc-order-item-tax-data-store.php b/includes/data-stores/class-wc-order-item-tax-data-store.php new file mode 100644 index 00000000000..5619143ddc5 --- /dev/null +++ b/includes/data-stores/class-wc-order-item-tax-data-store.php @@ -0,0 +1,61 @@ +get_id(); + $item->set_props( array( + 'rate_id' => get_metadata( 'order_item', $id, 'rate_id', true ), + 'label' => get_metadata( 'order_item', $id, 'label', true ), + 'compound' => get_metadata( 'order_item', $id, 'compound', true ), + 'tax_total' => get_metadata( 'order_item', $id, 'tax_amount', true ), + 'shipping_tax_total' => get_metadata( 'order_item', $id, 'shipping_tax_amount', true ), + ) ); + $item->set_object_read( true ); + } + + /** + * Saves an item's data to the database / item meta. + * Ran after both create and update, so $id will be set. + * + * @since 3.0.0 + * @param WC_Order_Item_Tax $item + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $save_values = array( + 'rate_id' => $item->get_rate_id( 'edit' ), + 'label' => $item->get_label( 'edit' ), + 'compound' => $item->get_compound( 'edit' ), + 'tax_amount' => $item->get_tax_total( 'edit' ), + 'shipping_tax_amount' => $item->get_shipping_tax_total( 'edit' ), + ); + foreach ( $save_values as $key => $value ) { + update_metadata( 'order_item', $id, $key, $value ); + } + } +} diff --git a/includes/data-stores/class-wc-order-refund-data-store-cpt.php b/includes/data-stores/class-wc-order-refund-data-store-cpt.php new file mode 100644 index 00000000000..b77592aab1e --- /dev/null +++ b/includes/data-stores/class-wc-order-refund-data-store-cpt.php @@ -0,0 +1,108 @@ +get_id(); + + if ( ! $id ) { + return; + } + + wp_delete_post( $id ); + $order->set_id( 0 ); + do_action( 'woocommerce_delete_order_refund', $id ); + } + + /** + * Read refund data. Can be overridden by child classes to load other props. + * + * @param WC_Order $refund + * @param object $post_object + * @since 3.0.0 + */ + protected function read_order_data( &$refund, $post_object ) { + parent::read_order_data( $refund, $post_object ); + $id = $refund->get_id(); + $refund->set_props( array( + 'amount' => get_post_meta( $id, '_refund_amount', true ), + 'refunded_by' => metadata_exists( 'post', $id, '_refunded_by' ) ? get_post_meta( $id, '_refunded_by', true ) : absint( $post_object->post_author ), + 'reason' => metadata_exists( 'post', $id, '_refund_reason' ) ? get_post_meta( $id, '_refund_reason', true ) : $post_object->post_excerpt, + ) ); + } + + /** + * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. + * + * @param WC_Order + * @param WC_Order $refund + * @since 3.0.0 + */ + protected function update_post_meta( &$refund ) { + parent::update_post_meta( $refund ); + + $updated_props = array(); + $meta_key_to_props = array( + '_refund_amount' => 'amount', + '_refunded_by' => 'refunded_by', + '_refund_reason' => 'reason', + ); + + $props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props ); + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $refund->{"get_$prop"}( 'edit' ); + update_post_meta( $refund->get_id(), $meta_key, $value ); + $updated_props[] = $prop; + } + + do_action( 'woocommerce_order_refund_object_updated_props', $refund, $updated_props ); + } + + /** + * Get a title for the new post type. + * + * @return string + */ + protected function get_post_title() { + // @codingStandardsIgnoreStart + /* translators: %s: Order date */ + return sprintf( __( 'Refund – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); + // @codingStandardsIgnoreEnd + } +} diff --git a/includes/data-stores/class-wc-payment-token-data-store.php b/includes/data-stores/class-wc-payment-token-data-store.php new file mode 100644 index 00000000000..ba56e929f06 --- /dev/null +++ b/includes/data-stores/class-wc-payment-token-data-store.php @@ -0,0 +1,339 @@ +validate() ) { + throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); + } + + global $wpdb; + if ( ! $token->is_default() && $token->get_user_id() > 0 ) { + $default_token = WC_Payment_Tokens::get_customer_default_token( $token->get_user_id() ); + if ( is_null( $default_token ) ) { + $token->set_default( true ); + } + } + + $payment_token_data = array( + 'gateway_id' => $token->get_gateway_id( 'edit' ), + 'token' => $token->get_token( 'edit' ), + 'user_id' => $token->get_user_id( 'edit' ), + 'type' => $token->get_type( 'edit' ), + ); + + $wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data ); + $token_id = $wpdb->insert_id; + $token->set_id( $token_id ); + $this->save_extra_data( $token, true ); + $token->save_meta_data(); + $token->apply_changes(); + + // Make sure all other tokens are not set to default + if ( $token->is_default() && $token->get_user_id() > 0 ) { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token_id ); + } + + do_action( 'woocommerce_new_payment_token', $token_id ); + } + + /** + * Update a payment token. + * + * @since 3.0.0 + * + * @param WC_Payment_Token $token + * + * @throws Exception + */ + public function update( &$token ) { + if ( false === $token->validate() ) { + throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); + } + + global $wpdb; + + $updated_props = array(); + $core_props = array( 'gateway_id', 'token', 'user_id', 'type' ); + $changed_props = array_keys( $token->get_changes() ); + + foreach ( $changed_props as $prop ) { + if ( ! in_array( $prop, $core_props ) ) { + continue; + } + $updated_props[] = $prop; + $payment_token_data[ $prop ] = $token->{"get_" . $prop}( 'edit' ); + } + + if ( ! empty( $payment_token_data ) ) { + $wpdb->update( + $wpdb->prefix . 'woocommerce_payment_tokens', + $payment_token_data, + array( 'token_id' => $token->get_id( 'edit' ) ) + ); + } + + $updated_extra_props = $this->save_extra_data( $token ); + $updated_props = array_merge( $updated_props, $updated_extra_props ); + $token->save_meta_data(); + $token->apply_changes(); + + // Make sure all other tokens are not set to default + if ( $token->is_default() && $token->get_user_id() > 0 ) { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token->get_id() ); + } + + do_action( 'woocommerce_payment_token_object_updated_props', $token, $updated_props ); + do_action( 'woocommerce_payment_token_updated', $token->get_id() ); + } + + /** + * Remove a payment token from the database. + * + * @since 3.0.0 + * @param WC_Payment_Token $token + * @param bool $force_delete + */ + public function delete( &$token, $force_delete = false ) { + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $token->get_id() ), array( '%d' ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $token->get_id() ), array( '%d' ) ); + do_action( 'woocommerce_payment_token_deleted', $token->get_id(), $token ); + } + + /** + * Read a token from the database. + * + * @since 3.0.0 + * + * @param WC_Payment_Token $token + * + * @throws Exception + */ + public function read( &$token ) { + global $wpdb; + if ( $data = $wpdb->get_row( $wpdb->prepare( "SELECT token, user_id, gateway_id, is_default FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1;", $token->get_id() ) ) ) { + $token->set_props( array( + 'token' => $data->token, + 'user_id' => $data->user_id, + 'gateway_id' => $data->gateway_id, + 'default' => $data->is_default, + ) ); + $this->read_extra_data( $token ); + $token->read_meta_data(); + $token->set_object_read( true ); + do_action( 'woocommerce_payment_token_loaded', $token ); + } else { + throw new Exception( __( 'Invalid payment token.', 'woocommerce' ) ); + } + } + + /** + * Read extra data associated with the token (like last4 digits of a card for expiry dates). + * + * @param WC_Payment_Token + * @since 3.0.0 + */ + protected function read_extra_data( &$token ) { + foreach ( $token->get_extra_data_keys() as $key ) { + $function = 'set_' . $key; + if ( is_callable( array( $token, $function ) ) ) { + $token->{$function}( get_metadata( 'payment_token', $token->get_id(), $key, true ) ); + } + } + } + + /** + * Saves extra token data as meta. + * + * @since 3.0.0 + * @param $token WC_Token + * @param $force bool + * @return array List of updated props. + */ + protected function save_extra_data( &$token, $force = false ) { + if ( $this->extra_data_saved ) { + return array(); + } + + $updated_props = array(); + $extra_data_keys = $token->get_extra_data_keys(); + $meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : array(); + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $token, $meta_key_to_props ); + + foreach ( $extra_data_keys as $key ) { + if ( ! array_key_exists( $key, $props_to_update ) ) { + continue; + } + $function = 'get_' . $key; + if ( is_callable( array( $token, $function ) ) ) { + if ( update_metadata( 'payment_token', $token->get_id(), $key, $token->{$function}( 'edit' ) ) ) { + $updated_props[] = $key; + } + } + } + + return $updated_props; + } + + /** + * Returns an array of objects (stdObject) matching specific token critera. + * Accepts token_id, user_id, gateway_id, and type. + * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param array $args + * @return array + */ + public function get_tokens( $args ) { + global $wpdb; + $args = wp_parse_args( $args, array( + 'token_id' => '', + 'user_id' => '', + 'gateway_id' => '', + 'type' => '', + ) ); + + $sql = "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens"; + $where = array( '1=1' ); + + if ( $args['token_id'] ) { + $token_ids = array_map( 'absint', is_array( $args['token_id'] ) ? $args['token_id'] : array( $args['token_id'] ) ); + $where[] = "token_id IN ('" . implode( "','", array_map( 'esc_sql', $token_ids ) ) . "')"; + } + + if ( $args['user_id'] ) { + $where[] = $wpdb->prepare( 'user_id = %d', absint( $args['user_id'] ) ); + } + + if ( $args['gateway_id'] ) { + $gateway_ids = array( $args['gateway_id'] ); + } else { + $gateways = WC_Payment_Gateways::instance(); + $gateway_ids = $gateways->get_payment_gateway_ids(); + } + + $gateway_ids[] = ''; + $where[] = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')"; + + if ( $args['type'] ) { + $where[] = $wpdb->prepare( 'type = %s', $args['type'] ); + } + + $token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) ); + + return $token_results; + } + + /** + * Returns an stdObject of a token for a user's default token. + * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param id $user_id + * @return object + */ + public function get_users_default_token( $user_id ) { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1", + $user_id + ) ); + } + + /** + * Returns an stdObject of a token. + * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param id $token_id + * @return object + */ + public function get_token_by_id( $token_id ) { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", + $token_id + ) ); + } + + /** + * Returns metadata for a specific payment token. + * + * @since 3.0.0 + * @param id $token_id + * @return array + */ + public function get_metadata( $token_id ) { + return get_metadata( 'payment_token', $token_id ); + } + + /** + * Get a token's type by ID. + * + * @since 3.0.0 + * @param id $token_id + * @return string + */ + public function get_token_type_by_id( $token_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( + "SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", + $token_id + ) ); + } + + /** + * Update's a tokens default status in the database. Used for quickly + * looping through tokens and setting their statuses instead of creating a bunch + * of objects. + * + * @since 3.0.0 + * + * @param id $token_id + * @param bool $status + * + * @return string + */ + public function set_default_status( $token_id, $status = true ) { + global $wpdb; + $wpdb->update( + $wpdb->prefix . 'woocommerce_payment_tokens', + array( 'is_default' => $status ), + array( + 'token_id' => $token_id, + ) + ); + } + +} diff --git a/includes/data-stores/class-wc-product-data-store-cpt.php b/includes/data-stores/class-wc-product-data-store-cpt.php new file mode 100644 index 00000000000..5fa2b9e0d4d --- /dev/null +++ b/includes/data-stores/class-wc-product-data-store-cpt.php @@ -0,0 +1,1375 @@ +get_date_created() ) { + $product->set_date_created( current_time( 'timestamp', true ) ); + } + + $id = wp_insert_post( apply_filters( 'woocommerce_new_product_data', array( + 'post_type' => 'product', + 'post_status' => $product->get_status() ? $product->get_status() : 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ), + 'post_content' => $product->get_description(), + 'post_excerpt' => $product->get_short_description(), + 'post_parent' => $product->get_parent_id(), + 'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed', + 'ping_status' => 'closed', + 'menu_order' => $product->get_menu_order(), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_name' => $product->get_slug( 'edit' ), + ) ), true ); + + if ( $id && ! is_wp_error( $id ) ) { + $product->set_id( $id ); + + $this->update_post_meta( $product, true ); + $this->update_terms( $product, true ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product, true ); + $this->update_version_and_type( $product ); + $this->handle_updated_props( $product ); + + $product->save_meta_data(); + $product->apply_changes(); + + $this->clear_caches( $product ); + + do_action( 'woocommerce_new_product', $id ); + } + } + + /** + * Method to read a product from the database. + * @param WC_Product $product + * @throws Exception + */ + public function read( &$product ) { + $product->set_defaults(); + + if ( ! $product->get_id() || ! ( $post_object = get_post( $product->get_id() ) ) || 'product' !== $post_object->post_type ) { + throw new Exception( __( 'Invalid product.', 'woocommerce' ) ); + } + + $id = $product->get_id(); + + $product->set_props( array( + 'name' => $post_object->post_title, + 'slug' => $post_object->post_name, + 'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null, + 'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null, + 'status' => $post_object->post_status, + 'description' => $post_object->post_content, + 'short_description' => $post_object->post_excerpt, + 'parent_id' => $post_object->post_parent, + 'menu_order' => $post_object->menu_order, + 'reviews_allowed' => 'open' === $post_object->comment_status, + ) ); + + $this->read_attributes( $product ); + $this->read_downloads( $product ); + $this->read_visibility( $product ); + $this->read_product_data( $product ); + $this->read_extra_data( $product ); + $product->set_object_read( true ); + } + + /** + * Method to update a product in the database. + * + * @param WC_Product $product + */ + public function update( &$product ) { + $product->save_meta_data(); + $changes = $product->get_changes(); + + // Only update the post when the post data changes. + if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_content' => $product->get_description( 'edit' ), + 'post_excerpt' => $product->get_short_description( 'edit' ), + 'post_title' => $product->get_name( 'edit' ), + 'post_parent' => $product->get_parent_id( 'edit' ), + 'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed', + 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', + 'menu_order' => $product->get_menu_order( 'edit' ), + 'post_name' => $product->get_slug( 'edit' ), + 'post_type' => 'product', + ); + if ( $product->get_date_created( 'edit' ) ) { + $post_data['post_date'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ); + $post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ); + } + if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) { + $post_data['post_modified'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ); + $post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ); + } else { + $post_data['post_modified'] = current_time( 'mysql' ); + $post_data['post_modified_gmt'] = current_time( 'mysql', 1 ); + } + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); + } + $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + } + + $this->update_post_meta( $product ); + $this->update_terms( $product ); + $this->update_visibility( $product ); + $this->update_attributes( $product ); + $this->update_version_and_type( $product ); + $this->handle_updated_props( $product ); + + $product->apply_changes(); + + $this->clear_caches( $product ); + + do_action( 'woocommerce_update_product', $product->get_id() ); + } + + /** + * Method to delete a product from the database. + * @param WC_Product $product + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$product, $args = array() ) { + $id = $product->get_id(); + $post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product'; + + $args = wp_parse_args( $args, array( + 'force_delete' => false, + ) ); + + if ( ! $id ) { + return; + } + + if ( $args['force_delete'] ) { + wp_delete_post( $id ); + $product->set_id( 0 ); + do_action( 'woocommerce_delete_' . $post_type, $id ); + } else { + wp_trash_post( $id ); + $product->set_status( 'trash' ); + do_action( 'woocommerce_trash_' . $post_type, $id ); + } + } + + /* + |-------------------------------------------------------------------------- + | Additional Methods + |-------------------------------------------------------------------------- + */ + + /** + * Read product data. Can be overridden by child classes to load other props. + * + * @param WC_Product + * @since 3.0.0 + */ + protected function read_product_data( &$product ) { + $id = $product->get_id(); + + if ( '' === ( $review_count = get_post_meta( $id, '_wc_review_count', true ) ) ) { + WC_Comments::get_review_count_for_product( $product ); + } else { + $product->set_review_count( $review_count ); + } + + if ( '' === ( $rating_counts = get_post_meta( $id, '_wc_rating_count', true ) ) ) { + WC_Comments::get_rating_counts_for_product( $product ); + } else { + $product->set_rating_counts( $rating_counts ); + } + + if ( '' === ( $average_rating = get_post_meta( $id, '_wc_average_rating', true ) ) ) { + WC_Comments::get_average_rating_for_product( $product ); + } else { + $product->set_average_rating( $average_rating ); + } + + $product->set_props( array( + 'sku' => get_post_meta( $id, '_sku', true ), + 'regular_price' => get_post_meta( $id, '_regular_price', true ), + 'sale_price' => get_post_meta( $id, '_sale_price', true ), + 'price' => get_post_meta( $id, '_price', true ), + 'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ), + 'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ), + 'total_sales' => get_post_meta( $id, 'total_sales', true ), + 'tax_status' => get_post_meta( $id, '_tax_status', true ), + 'tax_class' => get_post_meta( $id, '_tax_class', true ), + 'manage_stock' => get_post_meta( $id, '_manage_stock', true ), + 'stock_quantity' => get_post_meta( $id, '_stock', true ), + 'stock_status' => get_post_meta( $id, '_stock_status', true ), + 'backorders' => get_post_meta( $id, '_backorders', true ), + 'sold_individually' => get_post_meta( $id, '_sold_individually', true ), + 'weight' => get_post_meta( $id, '_weight', true ), + 'length' => get_post_meta( $id, '_length', true ), + 'width' => get_post_meta( $id, '_width', true ), + 'height' => get_post_meta( $id, '_height', true ), + 'upsell_ids' => get_post_meta( $id, '_upsell_ids', true ), + 'cross_sell_ids' => get_post_meta( $id, '_crosssell_ids', true ), + 'purchase_note' => get_post_meta( $id, '_purchase_note', true ), + 'default_attributes' => get_post_meta( $id, '_default_attributes', true ), + 'category_ids' => $this->get_term_ids( $product, 'product_cat' ), + 'tag_ids' => $this->get_term_ids( $product, 'product_tag' ), + 'shipping_class_id' => current( $this->get_term_ids( $product, 'product_shipping_class' ) ), + 'virtual' => get_post_meta( $id, '_virtual', true ), + 'downloadable' => get_post_meta( $id, '_downloadable', true ), + 'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ), + 'download_limit' => get_post_meta( $id, '_download_limit', true ), + 'download_expiry' => get_post_meta( $id, '_download_expiry', true ), + 'image_id' => get_post_thumbnail_id( $id ), + ) ); + } + + /** + * Read extra data associated with the product, like button text or product URL for external products. + * + * @param WC_Product + * @since 3.0.0 + */ + protected function read_extra_data( &$product ) { + foreach ( $product->get_extra_data_keys() as $key ) { + $function = 'set_' . $key; + if ( is_callable( array( $product, $function ) ) ) { + $product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) ); + } + } + } + + /** + * Convert visibility terms to props. + * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'. + * + * @param WC_Product + * @since 3.0.0 + */ + protected function read_visibility( &$product ) { + $terms = get_the_terms( $product->get_id(), 'product_visibility' ); + $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); + $featured = in_array( 'featured', $term_names ); + $exclude_search = in_array( 'exclude-from-search', $term_names ); + $exclude_catalog = in_array( 'exclude-from-catalog', $term_names ); + + if ( $exclude_search && $exclude_catalog ) { + $catalog_visibility = 'hidden'; + } elseif ( $exclude_search ) { + $catalog_visibility = 'catalog'; + } elseif ( $exclude_catalog ) { + $catalog_visibility = 'search'; + } else { + $catalog_visibility = 'visible'; + } + + $product->set_props( array( + 'featured' => $featured, + 'catalog_visibility' => $catalog_visibility, + ) ); + } + + /** + * Read attributes from post meta. + * + * @param WC_Product + * @since 3.0.0 + */ + protected function read_attributes( &$product ) { + $meta_values = get_post_meta( $product->get_id(), '_product_attributes', true ); + + if ( ! empty( $meta_values ) && is_array( $meta_values ) ) { + $attributes = array(); + foreach ( $meta_values as $meta_value ) { + $id = 0; + $meta_value = array_merge( array( + 'name' => '', + 'value' => '', + 'position' => 0, + 'is_visible' => 0, + 'is_variation' => 0, + 'is_taxonomy' => 0, + ), (array) $meta_value ); + + // Check if is a taxonomy attribute. + if ( ! empty( $meta_value['is_taxonomy'] ) ) { + if ( ! taxonomy_exists( $meta_value['name'] ) ) { + continue; + } + $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); + $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); + } else { + $options = wc_get_text_attributes( $meta_value['value'] ); + } + + $attribute = new WC_Product_Attribute(); + + $attribute->set_id( $id ); + $attribute->set_name( $meta_value['name'] ); + $attribute->set_options( $options ); + $attribute->set_position( $meta_value['position'] ); + $attribute->set_visible( $meta_value['is_visible'] ); + $attribute->set_variation( $meta_value['is_variation'] ); + $attributes[] = $attribute; + } + $product->set_attributes( $attributes ); + } + } + + /** + * Read downloads from post meta. + * + * @param WC_Product + * @since 3.0.0 + */ + protected function read_downloads( &$product ) { + $meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) ); + + if ( $meta_values ) { + $downloads = array(); + foreach ( $meta_values as $key => $value ) { + if ( ! isset( $value['name'], $value['file'] ) ) { + continue; + } + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) ); + $downloads[] = $download; + } + $product->set_downloads( $downloads ); + } + } + + /** + * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. + * + * @param WC_Product + * @param bool Force update. Used during create. + * @since 3.0.0 + */ + protected function update_post_meta( &$product, $force = false ) { + $meta_key_to_props = array( + '_sku' => 'sku', + '_regular_price' => 'regular_price', + '_sale_price' => 'sale_price', + '_sale_price_dates_from' => 'date_on_sale_from', + '_sale_price_dates_to' => 'date_on_sale_to', + 'total_sales' => 'total_sales', + '_tax_status' => 'tax_status', + '_tax_class' => 'tax_class', + '_manage_stock' => 'manage_stock', + '_backorders' => 'backorders', + '_sold_individually' => 'sold_individually', + '_weight' => 'weight', + '_length' => 'length', + '_width' => 'width', + '_height' => 'height', + '_upsell_ids' => 'upsell_ids', + '_crosssell_ids' => 'cross_sell_ids', + '_purchase_note' => 'purchase_note', + '_default_attributes' => 'default_attributes', + '_virtual' => 'virtual', + '_downloadable' => 'downloadable', + '_product_image_gallery' => 'gallery_image_ids', + '_download_limit' => 'download_limit', + '_download_expiry' => 'download_expiry', + '_thumbnail_id' => 'image_id', + '_stock' => 'stock_quantity', + '_stock_status' => 'stock_status', + '_wc_average_rating' => 'average_rating', + '_wc_rating_count' => 'rating_counts', + '_wc_review_count' => 'review_count', + ); + + // Make sure to take extra data (like product url or text for external products) into account. + $extra_data_keys = $product->get_extra_data_keys(); + + foreach ( $extra_data_keys as $key ) { + $meta_key_to_props[ '_' . $key ] = $key; + } + + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + switch ( $prop ) { + case 'virtual' : + case 'downloadable' : + case 'manage_stock' : + case 'sold_individually' : + $updated = update_post_meta( $product->get_id(), $meta_key, wc_bool_to_string( $value ) ); + break; + case 'gallery_image_ids' : + $updated = update_post_meta( $product->get_id(), $meta_key, implode( ',', $value ) ); + break; + case 'image_id' : + if ( ! empty( $value ) ) { + set_post_thumbnail( $product->get_id(), $value ); + } else { + delete_post_meta( $product->get_id(), '_thumbnail_id' ); + } + $updated = true; + break; + case 'date_on_sale_from' : + case 'date_on_sale_to' : + $updated = update_post_meta( $product->get_id(), $meta_key, $value ? $value->getTimestamp() : '' ); + break; + default : + $updated = update_post_meta( $product->get_id(), $meta_key, $value ); + break; + } + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + // Update extra data associated with the product like button text or product URL for external products. + if ( ! $this->extra_data_saved ) { + foreach ( $extra_data_keys as $key ) { + if ( ! array_key_exists( $key, $props_to_update ) ) { + continue; + } + $function = 'get_' . $key; + if ( is_callable( array( $product, $function ) ) ) { + if ( update_post_meta( $product->get_id(), '_' . $key, $product->{$function}( 'edit' ) ) ) { + $this->updated_props[] = $key; + } + } + } + } + + if ( $this->update_downloads( $product, $force ) ) { + $this->updated_props[] = 'downloads'; + } + } + + /** + * Handle updated meta props after updating meta data. + * + * @since 3.0.0 + * @param WC_Product $product + */ + protected function handle_updated_props( &$product ) { + if ( in_array( 'date_on_sale_from', $this->updated_props ) || in_array( 'date_on_sale_to', $this->updated_props ) || in_array( 'regular_price', $this->updated_props ) || in_array( 'sale_price', $this->updated_props ) ) { + if ( $product->is_on_sale( 'edit' ) ) { + update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) ); + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) ); + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + } + + if ( in_array( 'stock_quantity', $this->updated_props ) ) { + do_action( $product->is_type( 'variation' ) ? 'woocommerce_variation_set_stock' : 'woocommerce_product_set_stock' , $product ); + } + + if ( in_array( 'stock_status', $this->updated_props ) ) { + do_action( $product->is_type( 'variation' ) ? 'woocommerce_variation_set_stock_status' : 'woocommerce_product_set_stock_status' , $product->get_id(), $product->get_stock_status(), $product ); + } + + // Trigger action so 3rd parties can deal with updated props. + do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props ); + + // After handling, we can reset the props array. + $this->updated_props = array(); + } + + /** + * For all stored terms in all taxonomies, save them to the DB. + * + * @param WC_Product + * @param bool Force update. Used during create. + * @since 3.0.0 + */ + protected function update_terms( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'category_ids', $changes ) ) { + wp_set_post_terms( $product->get_id(), $product->get_category_ids( 'edit' ), 'product_cat', false ); + } + if ( $force || array_key_exists( 'tag_ids', $changes ) ) { + wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false ); + } + if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { + wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); + } + } + + /** + * Update visibility terms based on props. + * + * @since 3.0.0 + * + * @param WC_Product $product + * @param bool $force Force update. Used during create. + */ + protected function update_visibility( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) { + $terms = array(); + + if ( $product->get_featured() ) { + $terms[] = 'featured'; + } + + if ( 'outofstock' === $product->get_stock_status() ) { + $terms[] = 'outofstock'; + } + + $rating = min( 5, round( $product->get_average_rating(), 0 ) ); + + if ( $rating > 0 ) { + $terms[] = 'rated-' . $rating; + } + + switch ( $product->get_catalog_visibility() ) { + case 'hidden' : + $terms[] = 'exclude-from-search'; + $terms[] = 'exclude-from-catalog'; + break; + case 'catalog' : + $terms[] = 'exclude-from-search'; + break; + case 'search' : + $terms[] = 'exclude-from-catalog'; + break; + } + + if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) { + delete_transient( 'wc_featured_products' ); + do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() ); + } + } + } + + /** + * Update attributes which are a mix of terms and meta data. + * + * @param WC_Product + * @param bool Force update. Used during create. + * @since 3.0.0 + */ + protected function update_attributes( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'attributes', $changes ) ) { + $attributes = $product->get_attributes(); + $meta_values = array(); + + if ( $attributes ) { + foreach ( $attributes as $attribute_key => $attribute ) { + $value = ''; + + if ( is_null( $attribute ) ) { + if ( taxonomy_exists( $attribute_key ) ) { + // Handle attributes that have been unset. + wp_set_object_terms( $product->get_id(), array(), $attribute_key ); + } + continue; + + } elseif ( $attribute->is_taxonomy() ) { + wp_set_object_terms( $product->get_id(), wp_list_pluck( $attribute->get_terms(), 'term_id' ), $attribute->get_name() ); + } else { + $value = wc_implode_text_attributes( $attribute->get_options() ); + } + + // Store in format WC uses in meta. + $meta_values[ $attribute_key ] = array( + 'name' => $attribute->get_name(), + 'value' => $value, + 'position' => $attribute->get_position(), + 'is_visible' => $attribute->get_visible() ? 1 : 0, + 'is_variation' => $attribute->get_variation() ? 1 : 0, + 'is_taxonomy' => $attribute->is_taxonomy() ? 1 : 0, + ); + } + } + update_post_meta( $product->get_id(), '_product_attributes', $meta_values ); + } + } + + /** + * Update downloads. + * + * @since 3.0.0 + * @param WC_Product $product + * @param bool Force update. Used during create. + * @return bool If updated or not. + */ + protected function update_downloads( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'downloads', $changes ) ) { + $downloads = $product->get_downloads(); + $meta_values = array(); + + if ( $downloads ) { + foreach ( $downloads as $key => $download ) { + // Store in format WC uses in meta. + $meta_values[ $key ] = $download->get_data(); + } + } + + if ( $product->is_type( 'variation' ) ) { + do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads ); + } else { + do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads ); + } + + return update_post_meta( $product->get_id(), '_downloadable_files', $meta_values ); + } + return false; + } + + /** + * Make sure we store the product type and version (to track data changes). + * + * @param WC_Product + * @since 3.0.0 + */ + protected function update_version_and_type( &$product ) { + $old_type = WC_Product_Factory::get_product_type( $product->get_id() ); + $new_type = $product->get_type(); + + wp_set_object_terms( $product->get_id(), $new_type, 'product_type' ); + update_post_meta( $product->get_id(), '_product_version', WC_VERSION ); + + // Action for the transition. + if ( $old_type !== $new_type ) { + do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type ); + } + } + + /** + * Clear any caches. + * + * @param WC_Product + * @since 3.0.0 + */ + protected function clear_caches( &$product ) { + wc_delete_product_transients( $product->get_id() ); + } + + /* + |-------------------------------------------------------------------------- + | wc-product-functions.php methods + |-------------------------------------------------------------------------- + */ + + /** + * Returns an array of on sale products, as an array of objects with an + * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id. + * + * @return array + * @since 3.0.0 + */ + public function get_on_sale_products() { + global $wpdb; + + $decimals = absint( wc_get_price_decimals() ); + + return $wpdb->get_results( " + SELECT post.ID as id, post.post_parent as parent_id FROM `$wpdb->posts` AS post + LEFT JOIN `$wpdb->postmeta` AS meta ON post.ID = meta.post_id + LEFT JOIN `$wpdb->postmeta` AS meta2 ON post.ID = meta2.post_id + WHERE post.post_type IN ( 'product', 'product_variation' ) + AND post.post_status = 'publish' + AND meta.meta_key = '_sale_price' + AND meta2.meta_key = '_price' + AND CAST( meta.meta_value AS DECIMAL ) >= 0 + AND CAST( meta.meta_value AS CHAR ) != '' + AND CAST( meta.meta_value AS DECIMAL( 10, {$decimals} ) ) = CAST( meta2.meta_value AS DECIMAL( 10, {$decimals} ) ) + GROUP BY post.ID; + " ); + } + + /** + * Returns a list of product IDs ( id as key => parent as value) that are + * featured. Uses get_posts instead of wc_get_products since we want + * some extra meta queries and ALL products (posts_per_page = -1). + * + * @return array + * @since 3.0.0 + */ + public function get_featured_product_ids() { + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + return get_posts( array( + 'post_type' => array( 'product', 'product_variation' ), + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'tax_query' => array( + 'relation' => 'AND', + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['featured'] ), + ), + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), + 'operator' => 'NOT IN', + ), + ), + 'fields' => 'id=>parent', + ) ); + } + + /** + * Check if product sku is found for any other product IDs. + * + * @since 3.0.0 + * @param int $product_id + * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421 + * @return bool + */ + public function is_existing_sku( $product_id, $sku ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( " + SELECT $wpdb->posts.ID + FROM $wpdb->posts + LEFT JOIN $wpdb->postmeta ON ( $wpdb->posts.ID = $wpdb->postmeta.post_id ) + WHERE $wpdb->posts.post_type IN ( 'product', 'product_variation' ) + AND $wpdb->posts.post_status != 'trash' + AND $wpdb->postmeta.meta_key = '_sku' AND $wpdb->postmeta.meta_value = '%s' + AND $wpdb->postmeta.post_id <> %d LIMIT 1 + ", wp_slash( $sku ), $product_id ) ); + } + + /** + * Return product ID based on SKU. + * + * @since 3.0.0 + * @param string $sku + * @return int + */ + public function get_product_id_by_sku( $sku ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( " + SELECT posts.ID + FROM $wpdb->posts AS posts + LEFT JOIN $wpdb->postmeta AS postmeta ON ( posts.ID = postmeta.post_id ) + WHERE posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status != 'trash' + AND postmeta.meta_key = '_sku' + AND postmeta.meta_value = '%s' + LIMIT 1 + ", $sku ) ); + } + + /** + * Returns an array of IDs of products that have sales starting soon. + * + * @since 3.0.0 + * @return array + */ + public function get_starting_sales() { + global $wpdb; + return $wpdb->get_col( $wpdb->prepare( " + SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta + LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id + LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id + WHERE postmeta.meta_key = '_sale_price_dates_from' + AND postmeta_2.meta_key = '_price' + AND postmeta_3.meta_key = '_sale_price' + AND postmeta.meta_value > 0 + AND postmeta.meta_value < %s + AND postmeta_2.meta_value != postmeta_3.meta_value + ", current_time( 'timestamp', true ) ) ); + } + + /** + * Returns an array of IDs of products that have sales which are due to end. + * + * @since 3.0.0 + * @return array + */ + public function get_ending_sales() { + global $wpdb; + return $wpdb->get_col( $wpdb->prepare( " + SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta + LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id + LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id + WHERE postmeta.meta_key = '_sale_price_dates_to' + AND postmeta_2.meta_key = '_price' + AND postmeta_3.meta_key = '_regular_price' + AND postmeta.meta_value > 0 + AND postmeta.meta_value < %s + AND postmeta_2.meta_value != postmeta_3.meta_value + ", current_time( 'timestamp', true ) ) ); + } + + /** + * Find a matching (enabled) variation within a variable product. + * + * @since 3.0.0 + * @param WC_Product $product Variable product. + * @param array $match_attributes Array of attributes we want to try to match. + * @return int Matching variation ID or 0. + */ + public function find_matching_product_variation( $product, $match_attributes = array() ) { + $query_args = array( + 'post_parent' => $product->get_id(), + 'post_type' => 'product_variation', + 'orderby' => 'menu_order', + 'order' => 'ASC', + 'fields' => 'ids', + 'post_status' => 'publish', + 'numberposts' => 1, + 'meta_query' => array(), + ); + + // Allow large queries in case user has many variations or attributes. + $GLOBALS['wpdb']->query( 'SET SESSION SQL_BIG_SELECTS=1' ); + + foreach ( $product->get_attributes() as $attribute ) { + if ( ! $attribute->get_variation() ) { + continue; + } + + $attribute_field_name = 'attribute_' . sanitize_title( $attribute->get_name() ); + + if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) { + return 0; + } + + // Note not wc_clean here to prevent removal of entities. + $value = $match_attributes[ $attribute_field_name ]; + + $query_args['meta_query'][] = array( + 'relation' => 'OR', + array( + 'key' => $attribute_field_name, + 'value' => array( '', $value ), + 'compare' => 'IN', + ), + array( + 'key' => $attribute_field_name, + 'compare' => 'NOT EXISTS', + ) + ); + } + + $variations = get_posts( $query_args ); + + if ( $variations && ! is_wp_error( $variations ) ) { + return current( $variations ); + } elseif ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { + /** + * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. + * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same. + */ + return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) ); + } + + return 0; + } + + /** + * Make sure all variations have a sort order set so they can be reordered correctly. + * + * @param int $parent_id + */ + public function sort_all_product_variations( $parent_id ) { + global $wpdb; + $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC", $parent_id ) ); + $index = 1; + + foreach ( $ids as $id ) { + $wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) ); + } + } + + /** + * Return a list of related products (using data like categories and IDs). + * + * @since 3.0.0 + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * @param int $product_id + * @return array + */ + public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) { + global $wpdb; + return $wpdb->get_col( implode( ' ', apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id ) ) ); + } + + /** + * Builds the related posts query. + * + * @since 3.0.0 + * + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * + * @return array + */ + public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) { + global $wpdb; + + $include_term_ids = array_merge( $cats_array, $tags_array ); + $exclude_term_ids = array(); + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + if ( $product_visibility_term_ids['exclude-from-catalog'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; + } + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; + } + + $query = array( + 'fields' => " + SELECT DISTINCT ID FROM {$wpdb->posts} p + ", + 'join' => '', + 'where' => " + WHERE 1=1 + AND p.post_status = 'publish' + AND p.post_type = 'product' + + ", + 'limits' => " + LIMIT " . absint( $limit ) . " + ", + ); + + if ( count( $exclude_term_ids ) ) { + $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . " ) ) AS exclude_join ON exclude_join.object_id = p.ID"; + $query['where'] .= " AND exclude_join.object_id IS NULL"; + } + + if ( count( $include_term_ids ) ) { + $query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . " ) ) AS include_join ON include_join.object_id = p.ID"; + } + + if ( count( $exclude_ids ) ) { + $query['where'] .= " AND p.ID NOT IN ( " . implode( ',', array_map( 'absint', $exclude_ids ) ) . " )"; + } + + return $query; + } + + /** + * Update a product's stock amount directly. + * + * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). + * + * @since 3.0.0 this supports set, increase and decrease. + * @param int + * @param int|null $stock_quantity + * @param string $operation set, increase and decrease. + */ + public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) { + global $wpdb; + add_post_meta( $product_id_with_stock, '_stock', 0, true ); + + // Update stock in DB directly + switch ( $operation ) { + case 'increase' : + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='_stock'", $stock_quantity, $product_id_with_stock ) ); + break; + case 'decrease' : + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='_stock'", $stock_quantity, $product_id_with_stock ) ); + break; + default : + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", $stock_quantity, $product_id_with_stock ) ); + break; + } + + wp_cache_delete( $product_id_with_stock, 'post_meta' ); + } + + /** + * Update a product's sale count directly. + * + * Uses queries rather than update_post_meta so we can do this in one query for performance. + * + * @since 3.0.0 this supports set, increase and decrease. + * @param int + * @param int|null $quantity + * @param string $operation set, increase and decrease. + */ + public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) { + global $wpdb; + add_post_meta( $product_id, 'total_sales', 0, true ); + + // Update stock in DB directly + switch ( $operation ) { + case 'increase' : + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id ) ); + break; + case 'decrease' : + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id ) ); + break; + default : + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'", $quantity, $product_id ) ); + break; + } + + wp_cache_delete( $product_id, 'post_meta' ); + } + + /** + * Update a products average rating meta. + * + * @since 3.0.0 + * @param WC_Product $product + */ + public function update_average_rating( $product ) { + update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) ); + self::update_visibility( $product, true ); + } + + /** + * Update a products review count meta. + * + * @since 3.0.0 + * @param WC_Product $product + */ + public function update_review_count( $product ) { + update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) ); + } + + /** + * Update a products rating counts. + * + * @since 3.0.0 + * @param WC_Product $product + */ + public function update_rating_counts( $product ) { + update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) ); + } + + /** + * Get shipping class ID by slug. + * + * @since 3.0.0 + * @param $slug string + * @return int|false + */ + public function get_shipping_class_id_by_slug( $slug ) { + $shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' ); + if ( $shipping_class_term ) { + return $shipping_class_term->term_id; + } else { + return false; + } + } + + /** + * Returns an array of products. + * + * @param array $args @see wc_get_products + * + * @return array|object + */ + public function get_products( $args = array() ) { + /** + * Generate WP_Query args. + */ + $wp_query_args = array( + 'post_status' => $args['status'], + 'posts_per_page' => $args['limit'], + 'meta_query' => array(), + 'orderby' => $args['orderby'], + 'order' => $args['order'], + 'tax_query' => array(), + ); + + if ( 'variation' === $args['type'] ) { + $wp_query_args['post_type'] = 'product_variation'; + } elseif ( is_array( $args['type'] ) && in_array( 'variation', $args['type'] ) ) { + $wp_query_args['post_type'] = array( 'product_variation', 'product' ); + $wp_query_args['tax_query'][] = array( + 'relation' => 'OR', + array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $args['type'], + ), + array( + 'taxonomy' => 'product_type', + 'field' => 'id', + 'operator' => 'NOT EXISTS', + ), + ); + } else { + $wp_query_args['post_type'] = 'product'; + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $args['type'], + ); + } + + // Do not load unnecessary post data if the user only wants IDs. + if ( 'ids' === $args['return'] ) { + $wp_query_args['fields'] = 'ids'; + } + + if ( ! empty( $args['sku'] ) ) { + $wp_query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $args['sku'], + 'compare' => 'LIKE', + ); + } + + if ( ! empty( $args['category'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_cat', + 'field' => 'slug', + 'terms' => $args['category'], + ); + } + + if ( ! empty( $args['tag'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_tag', + 'field' => 'slug', + 'terms' => $args['tag'], + ); + } + + if ( ! empty( $args['shipping_class'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_shipping_class', + 'field' => 'slug', + 'terms' => $args['shipping_class'], + ); + } + + if ( ! is_null( $args['parent'] ) ) { + $wp_query_args['post_parent'] = absint( $args['parent'] ); + } + + if ( ! is_null( $args['offset'] ) ) { + $wp_query_args['offset'] = absint( $args['offset'] ); + } else { + $wp_query_args['paged'] = absint( $args['page'] ); + } + + if ( ! empty( $args['include'] ) ) { + $wp_query_args['post__in'] = array_map( 'absint', $args['include'] ); + } + + if ( ! empty( $args['exclude'] ) ) { + $wp_query_args['post__not_in'] = array_map( 'absint', $args['exclude'] ); + } + + if ( ! $args['paginate'] ) { + $wp_query_args['no_found_rows'] = true; + } + + // Get results. + $products = new WP_Query( $wp_query_args ); + + if ( 'objects' === $args['return'] ) { + // Prime caches before grabbing objects. + update_post_caches( $products->posts, array( 'product', 'product_variation' ) ); + + $return = array_filter( array_map( 'wc_get_product', $products->posts ) ); + } else { + $return = $products->posts; + } + + if ( $args['paginate'] ) { + return (object) array( + 'products' => $return, + 'total' => $products->found_posts, + 'max_num_pages' => $products->max_num_pages, + ); + } else { + return $return; + } + } + + /** + * Search product data for a term and return ids. + * + * @param string $term + * @param string $type of product + * @param bool $include_variations in search or not + * @return array of ids + */ + public function search_products( $term, $type = '', $include_variations = false ) { + global $wpdb; + + $search_fields = array_map( 'wc_clean', apply_filters( 'woocommerce_product_search_fields', array( + '_sku', + ) ) ); + $like_term = '%' . $wpdb->esc_like( $term ) . '%'; + $post_types = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' ); + $post_statuses = current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' ); + $type_join = ''; + $type_where = ''; + + if ( $type ) { + if ( in_array( $type, array( 'virtual', 'downloadable' ) ) ) { + $type_join = " LEFT JOIN {$wpdb->postmeta} postmeta_type ON posts.ID = postmeta_type.post_id "; + $type_where = " AND ( postmeta_type.meta_key = '_{$type}' AND postmeta_type.meta_value = 'yes' ) "; + } + } + + $product_ids = $wpdb->get_col( + $wpdb->prepare( " + SELECT DISTINCT posts.ID FROM {$wpdb->posts} posts + LEFT JOIN {$wpdb->postmeta} postmeta ON posts.ID = postmeta.post_id + $type_join + WHERE ( + posts.post_title LIKE %s + OR posts.post_content LIKE %s + OR ( + postmeta.meta_key = '_sku' AND postmeta.meta_value LIKE %s + ) + ) + AND posts.post_type IN ('" . implode( "','", $post_types ) . "') + AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') + $type_where + ORDER BY posts.post_parent ASC, posts.post_title ASC + ", + $like_term, + $like_term, + $like_term + ) + ); + + if ( is_numeric( $term ) ) { + $post_id = absint( $term ); + $post_type = get_post_type( $post_id ); + + if ( 'product_variation' === $post_type && $include_variations ) { + $product_ids[] = $post_id; + } elseif ( 'product' === $post_type ) { + $product_ids[] = $post_id; + } + + $product_ids[] = wp_get_post_parent_id( $post_id ); + } + + return wp_parse_id_list( $product_ids ); + } + + /** + * Get the product type based on product ID. + * + * @since 3.0.0 + * @param int $product_id + * @return bool|string + */ + public function get_product_type( $product_id ) { + $post_type = get_post_type( $product_id ); + if ( 'product_variation' === $post_type ) { + return 'variation'; + } elseif ( 'product' === $post_type ) { + $terms = get_the_terms( $product_id, 'product_type' ); + return ! empty( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple'; + } else { + return false; + } + } +} diff --git a/includes/data-stores/class-wc-product-grouped-data-store-cpt.php b/includes/data-stores/class-wc-product-grouped-data-store-cpt.php new file mode 100644 index 00000000000..d9677c4898e --- /dev/null +++ b/includes/data-stores/class-wc-product-grouped-data-store-cpt.php @@ -0,0 +1,93 @@ + 'children', + ); + + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + $updated = update_post_meta( $product->get_id(), $meta_key, $value ); + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + parent::update_post_meta( $product, $force ); + } + + /** + * Handle updated meta props after updating meta data. + * + * @since 3.0.0 + * @param WC_Product $product + */ + protected function handle_updated_props( &$product ) { + if ( in_array( 'children', $this->updated_props ) ) { + $child_prices = array(); + foreach ( $product->get_children( 'edit' ) as $child_id ) { + $child = wc_get_product( $child_id ); + if ( $child ) { + $child_prices[] = $child->get_price(); + } + } + $child_prices = array_filter( $child_prices ); + delete_post_meta( $product->get_id(), '_price' ); + + if ( ! empty( $child_prices ) ) { + add_post_meta( $product->get_id(), '_price', min( $child_prices ) ); + add_post_meta( $product->get_id(), '_price', max( $child_prices ) ); + } + } + parent::handle_updated_props( $product ); + } + + /** + * Sync grouped product prices with children. + * + * @since 3.0.0 + * @param WC_Product|int $product + */ + public function sync_price( &$product ) { + global $wpdb; + + $children_ids = get_posts( array( + 'post_parent' => $product->get_id(), + 'post_type' => 'product', + 'fields' => 'ids', + ) ); + $prices = $children_ids ? array_unique( $wpdb->get_col( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN ( " . implode( ',', array_map( 'absint', $children_ids ) ) . " )" ) ) : array(); + + delete_post_meta( $product->get_id(), '_price' ); + delete_transient( 'wc_var_prices_' . $product->get_id() ); + + if ( $prices ) { + sort( $prices ); + // To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner. + foreach ( $prices as $price ) { + add_post_meta( $product->get_id(), '_price', $price, false ); + } + } + } +} diff --git a/includes/data-stores/class-wc-product-variable-data-store-cpt.php b/includes/data-stores/class-wc-product-variable-data-store-cpt.php new file mode 100644 index 00000000000..dab9847dbdf --- /dev/null +++ b/includes/data-stores/class-wc-product-variable-data-store-cpt.php @@ -0,0 +1,445 @@ +read_children( $product ); + $product->set_children( $children['all'] ); + $product->set_visible_children( $children['visible'] ); + $product->set_variation_attributes( $this->read_variation_attributes( $product ) ); + } + + /** + * Loads variation child IDs. + * + * @param WC_Product + * @param bool $force_read True to bypass the transient. + * @return array + */ + protected function read_children( &$product, $force_read = false ) { + $children_transient_name = 'wc_product_children_' . $product->get_id(); + $children = get_transient( $children_transient_name ); + + if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) { + $all_args = $visible_only_args = array( + 'post_parent' => $product->get_id(), + 'post_type' => 'product_variation', + 'orderby' => array( 'menu_order' => 'ASC', 'ID' => 'ASC' ), + 'fields' => 'ids', + 'post_status' => 'publish', + 'numberposts' => -1, + ); + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $visible_only_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'outofstock', + 'operator' => 'NOT IN', + ); + } + $children['all'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) ); + $children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) ); + + set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 ); + } + + $children['all'] = wp_parse_id_list( (array) $children['all'] ); + $children['visible'] = wp_parse_id_list( (array) $children['visible'] ); + + return $children; + } + + /** + * Loads an array of attributes used for variations, as well as their possible values. + * + * @param WC_Product + * @return array + */ + protected function read_variation_attributes( &$product ) { + global $wpdb; + + $variation_attributes = array(); + $attributes = $product->get_attributes(); + $child_ids = $product->get_children(); + + if ( ! empty( $child_ids ) && ! empty( $attributes ) ) { + foreach ( $attributes as $attribute ) { + if ( empty( $attribute['is_variation'] ) ) { + continue; + } + + // Get possible values for this attribute, for only visible variations. + $values = array_unique( $wpdb->get_col( $wpdb->prepare( + "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN (" . implode( ',', array_map( 'absint', $child_ids ) ) . ")", + wc_variation_attribute_name( $attribute['name'] ) + ) ) ); + + // Empty value indicates that all options for given attribute are available. + if ( in_array( '', $values ) || empty( $values ) ) { + $values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] ); + // Get custom attributes (non taxonomy) as defined. + } elseif ( ! $attribute['is_taxonomy'] ) { + $text_attributes = wc_get_text_attributes( $attribute['value'] ); + $assigned_text_attributes = $values; + $values = array(); + + // Pre 2.4 handling where 'slugs' were saved instead of the full text attribute + if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { + $assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes ); + foreach ( $text_attributes as $text_attribute ) { + if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes ) ) { + $values[] = $text_attribute; + } + } + } else { + foreach ( $text_attributes as $text_attribute ) { + if ( in_array( $text_attribute, $assigned_text_attributes ) ) { + $values[] = $text_attribute; + } + } + } + } + $variation_attributes[ $attribute['name'] ] = array_unique( $values ); + } + } + + return $variation_attributes; + } + + /** + * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. + * + * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter. + * This is to ensure modified prices are not cached, unless intended. + * + * @since 3.0.0 + * @param WC_Product + * @param bool $include_taxes If taxes should be calculated or not. + * @return array of prices + */ + public function read_price_data( &$product, $include_taxes = false ) { + + /** + * Transient name for storing prices for this product (note: Max transient length is 45) + * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product. + */ + $transient_name = 'wc_var_prices_' . $product->get_id(); + + $price_hash = $this->get_price_hash( $product, $include_taxes ); + + /** + * $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array. + * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered. + */ + if ( empty( $this->prices_array[ $price_hash ] ) ) { + $transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) ); + + // If the product version has changed since the transient was last saved, reset the transient cache. + if ( empty( $transient_cached_prices_array['version'] ) || WC_Cache_Helper::get_transient_version( 'product' ) !== $transient_cached_prices_array['version'] ) { + $transient_cached_prices_array = array( 'version' => WC_Cache_Helper::get_transient_version( 'product' ) ); + } + + // If the prices are not stored for this hash, generate them and add to the transient. + if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) { + $prices = array(); + $regular_prices = array(); + $sale_prices = array(); + $variation_ids = $product->get_visible_children(); + foreach ( $variation_ids as $variation_id ) { + if ( $variation = wc_get_product( $variation_id ) ) { + $price = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product ); + $regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product ); + $sale_price = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product ); + + // Skip empty prices + if ( '' === $price ) { + continue; + } + + // If sale price does not equal price, the product is not yet on sale + if ( $sale_price === $regular_price || $sale_price !== $price ) { + $sale_price = $regular_price; + } + + // If we are getting prices for display, we need to account for taxes + if ( $include_taxes ) { + if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) { + $price = '' === $price ? '' : wc_get_price_including_tax( $variation, array( 'qty' => 1, 'price' => $price ) ); + $regular_price = '' === $regular_price ? '' : wc_get_price_including_tax( $variation, array( 'qty' => 1, 'price' => $regular_price ) ); + $sale_price = '' === $sale_price ? '' : wc_get_price_including_tax( $variation, array( 'qty' => 1, 'price' => $sale_price ) ); + } else { + $price = '' === $price ? '' : wc_get_price_excluding_tax( $variation, array( 'qty' => 1, 'price' => $price ) ); + $regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax( $variation, array( 'qty' => 1, 'price' => $regular_price ) ); + $sale_price = '' === $sale_price ? '' : wc_get_price_excluding_tax( $variation, array( 'qty' => 1, 'price' => $sale_price ) ); + } + } + + $prices[ $variation_id ] = wc_format_decimal( $price, wc_get_price_decimals() ); + $regular_prices[ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() ); + $sale_prices[ $variation_id ] = wc_format_decimal( $sale_price . '.00', wc_get_price_decimals() ); + } + } + + $transient_cached_prices_array[ $price_hash ] = array( + 'price' => $prices, + 'regular_price' => $regular_prices, + 'sale_price' => $sale_prices, + ); + + set_transient( $transient_name, json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 ); + } + + /** + * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class. + * This value may differ from the transient cache. It is filtered once before storing locally. + */ + $this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $include_taxes ); + } + return $this->prices_array[ $price_hash ]; + } + + /** + * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters. + * DEVELOPERS should filter this hash if offering conditonal pricing to keep it unique. + * + * @since 3.0.0 + * @param WC_Product + * @param bool $include_taxes If taxes should be calculated or not. + * @return string + */ + protected function get_price_hash( &$product, $include_taxes = false ) { + global $wp_filter; + + $price_hash = $include_taxes ? array( get_option( 'woocommerce_tax_display_shop', 'excl' ), WC_Tax::get_rates() ) : array( false ); + $filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' ); + + foreach ( $filter_names as $filter_name ) { + if ( ! empty( $wp_filter[ $filter_name ] ) ) { + $price_hash[ $filter_name ] = array(); + + foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) { + $price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) ); + } + } + } + + $price_hash[] = WC_Cache_Helper::get_transient_version( 'product' ); + $price_hash = md5( json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $include_taxes ) ) ); + + return $price_hash; + } + + /** + * Does a child have a weight set? + * + * @since 3.0.0 + * @param WC_Product + * @return boolean + */ + public function child_has_weight( $product ) { + global $wpdb; + $children = $product->get_visible_children(); + return $children ? null !== $wpdb->get_var( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN ( " . implode( ',', array_map( 'absint', $children ) ) . " )" ) : false; + } + + /** + * Does a child have dimensions set? + * + * @since 3.0.0 + * @param WC_Product + * @return boolean + */ + public function child_has_dimensions( $product ) { + global $wpdb; + $children = $product->get_visible_children(); + return $children ? null !== $wpdb->get_var( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN ( " . implode( ',', array_map( 'absint', $children ) ) . " )" ) : false; + } + + /** + * Is a child in stock? + * + * @since 3.0.0 + * @param WC_Product + * @return boolean + */ + public function child_is_in_stock( $product ) { + global $wpdb; + $children = $product->get_children(); + $oufofstock_children = $children ? $wpdb->get_var( "SELECT COUNT( post_id ) FROM $wpdb->postmeta WHERE meta_key = '_stock_status' AND meta_value = 'outofstock' AND post_id IN ( " . implode( ',', array_map( 'absint', $children ) ) . " )" ) : 0; + return count( $children ) > $oufofstock_children; + } + + /** + * Syncs all variation names if the parent name is changed. + * + * @param WC_Product $product + * @param string $previous_name + * @param string $new_name + * @since 3.0.0 + */ + public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) { + if ( $new_name !== $previous_name ) { + global $wpdb; + + $wpdb->query( $wpdb->prepare( + " + UPDATE {$wpdb->posts} + SET post_title = REPLACE( post_title, %s, %s ) + WHERE post_type = 'product_variation' + AND post_parent = %d + ", + $previous_name ? $previous_name : 'AUTO-DRAFT', + $new_name, + $product->get_id() + ) ); + } + } + + /** + * Stock managed at the parent level - update children being managed by this product. + * This sync function syncs downwards (from parent to child) when the variable product is saved. + * + * @param WC_Product + * @since 3.0.0 + */ + public function sync_managed_variation_stock_status( &$product ) { + global $wpdb; + + if ( $product->get_manage_stock() ) { + $status = $product->get_stock_status(); + $children = $product->get_children(); + $managed_children = $children ? array_unique( $wpdb->get_col( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN ( " . implode( ',', array_map( 'absint', $children ) ) . " )" ) ) : array(); + $changed = false; + foreach ( $managed_children as $managed_child ) { + if ( update_post_meta( $managed_child, '_stock_status', $status ) ) { + $changed = true; + } + } + if ( $changed ) { + $children = $this->read_children( $product, true ); + $product->set_children( $children['all'] ); + $product->set_visible_children( $children['visible'] ); + } + } + } + + /** + * Sync variable product prices with children. + * + * @since 3.0.0 + * @param WC_Product|int $product + */ + public function sync_price( &$product ) { + global $wpdb; + + $children = $product->get_visible_children(); + $prices = $children ? array_unique( $wpdb->get_col( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN ( " . implode( ',', array_map( 'absint', $children ) ) . " )" ) ) : array(); + + delete_post_meta( $product->get_id(), '_price' ); + + if ( $prices ) { + sort( $prices ); + // To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner. + foreach ( $prices as $price ) { + if ( is_null( $price ) || '' === $price ) { + continue; + } + add_post_meta( $product->get_id(), '_price', $price, false ); + } + } + } + + /** + * Sync variable product stock status with children. + * Change does not persist unless saved by caller. + * + * @since 3.0.0 + * @param WC_Product|int $product + */ + public function sync_stock_status( &$product ) { + $product->set_stock_status( $product->child_is_in_stock() ? 'instock' : 'outofstock' ); + } + + /** + * Delete variations of a product. + * + * @since 3.0.0 + * @param int $product_id + * @param bool $force_delete False to trash. + */ + public function delete_variations( $product_id, $force_delete = false ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + $variation_ids = wp_parse_id_list( get_posts( array( + 'post_parent' => $product_id, + 'post_type' => 'product_variation', + 'fields' => 'ids', + 'post_status' => array( 'any', 'trash', 'auto-draft' ), + 'numberposts' => -1, + ) ) ); + + if ( ! empty( $variation_ids ) ) { + foreach ( $variation_ids as $variation_id ) { + if ( $force_delete ) { + wp_delete_post( $variation_id, true ); + } else { + wp_trash_post( $variation_id ); + } + } + } + + delete_transient( 'wc_product_children_' . $product_id ); + } + + /** + * Untrash variations. + * + * @param int $product_id + */ + public function untrash_variations( $product_id ) { + $variation_ids = wp_parse_id_list( get_posts( array( + 'post_parent' => $product_id, + 'post_type' => 'product_variation', + 'fields' => 'ids', + 'post_status' => 'trash', + 'numberposts' => -1, + ) ) ); + + if ( ! empty( $variation_ids ) ) { + foreach ( $variation_ids as $variation_id ) { + wp_untrash_post( $variation_id ); + } + } + + delete_transient( 'wc_product_children_' . $product_id ); + } +} diff --git a/includes/data-stores/class-wc-product-variation-data-store-cpt.php b/includes/data-stores/class-wc-product-variation-data-store-cpt.php new file mode 100644 index 00000000000..c1d69cf2910 --- /dev/null +++ b/includes/data-stores/class-wc-product-variation-data-store-cpt.php @@ -0,0 +1,429 @@ +meta_key, $this->internal_meta_keys ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD Methods + |-------------------------------------------------------------------------- + */ + + /** + * Reads a product from the database and sets its data to the class. + * + * @since 3.0.0 + * @param WC_Product $product + * @throws Exception + */ + public function read( &$product ) { + $product->set_defaults(); + + if ( ! $product->get_id() || ! ( $post_object = get_post( $product->get_id() ) ) || ! in_array( $post_object->post_type, array( 'product', 'product_variation' ) ) ) { + return; + } + + $product->set_props( array( + 'name' => $post_object->post_title, + 'slug' => $post_object->post_name, + 'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null, + 'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null, + 'status' => $post_object->post_status, + 'menu_order' => $post_object->menu_order, + 'reviews_allowed' => 'open' === $post_object->comment_status, + 'parent_id' => $post_object->post_parent, + ) ); + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $this->read_downloads( $product ); + $this->read_product_data( $product ); + $this->read_extra_data( $product ); + $product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) ); + + /** + * If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update. + */ + $new_title = $this->generate_product_title( $product ); + + if ( $post_object->post_title !== $new_title ) { + $product->set_name( $new_title ); + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, array( 'post_title' => $new_title ), array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } + + // Set object_read true once all data is read. + $product->set_object_read( true ); + } + + /** + * Create a new product. + * + * @since 3.0.0 + * @param WC_Product $product + */ + public function create( &$product ) { + if ( ! $product->get_date_created() ) { + $product->set_date_created( current_time( 'timestamp', true ) ); + } + + $new_title = $this->generate_product_title( $product ); + + if ( $product->get_name( 'edit' ) !== $new_title ) { + $product->set_name( $new_title ); + } + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $id = wp_insert_post( apply_filters( 'woocommerce_new_product_variation_data', array( + 'post_type' => 'product_variation', + 'post_status' => $product->get_status() ? $product->get_status() : 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $product->get_name( 'edit' ), + 'post_content' => '', + 'post_parent' => $product->get_parent_id(), + 'comment_status' => 'closed', + 'ping_status' => 'closed', + 'menu_order' => $product->get_menu_order(), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_name' => $product->get_slug( 'edit' ), + ) ), true ); + + if ( $id && ! is_wp_error( $id ) ) { + $product->set_id( $id ); + + $this->update_post_meta( $product, true ); + $this->update_terms( $product, true ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product, true ); + $this->handle_updated_props( $product ); + + $product->save_meta_data(); + $product->apply_changes(); + + $this->update_version_and_type( $product ); + + $this->clear_caches( $product ); + + do_action( 'woocommerce_new_product_variation', $id ); + } + } + + /** + * Updates an existing product. + * + * @since 3.0.0 + * @param WC_Product $product + */ + public function update( &$product ) { + $product->save_meta_data(); + + if ( ! $product->get_date_created() ) { + $product->set_date_created( current_time( 'timestamp', true ) ); + } + + $new_title = $this->generate_product_title( $product ); + + if ( $product->get_name( 'edit' ) !== $new_title ) { + $product->set_name( $new_title ); + } + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $changes = $product->get_changes(); + + // Only update the post when the post data changes. + if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_title' => $product->get_name( 'edit' ), + 'post_parent' => $product->get_parent_id( 'edit' ), + 'comment_status' => 'closed', + 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', + 'menu_order' => $product->get_menu_order( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), + 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), + 'post_type' => 'product_variation', + 'post_name' => $product->get_slug( 'edit' ), + ); + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); + } + $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + } + + $this->update_post_meta( $product ); + $this->update_terms( $product ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product ); + $this->handle_updated_props( $product ); + + $product->apply_changes(); + + $this->update_version_and_type( $product ); + + $this->clear_caches( $product ); + + do_action( 'woocommerce_update_product_variation', $product->get_id() ); + } + + /* + |-------------------------------------------------------------------------- + | Additional Methods + |-------------------------------------------------------------------------- + */ + + /** + * Generates a title with attribute information for a variation. + * Products will get a title of the form "Name - Value, Value" or just "Name". + * + * @since 3.0.0 + * @param WC_Product + * @return string + */ + protected function generate_product_title( $product ) { + $attributes = (array) $product->get_attributes(); + + // Do not include attributes if the product has 3+ attributes. + $should_include_attributes = count( $attributes ) < 3; + + // Do not include attributes if an attribute name has 2+ words and the + // product has multiple attributes. + if ( $should_include_attributes && 1 < count( $attributes ) ) { + foreach ( $attributes as $name => $value ) { + if ( false !== strpos( $name, '-' ) ) { + $should_include_attributes = false; + break; + } + } + } + + $should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product ); + $separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product ); + $title_base = get_post_field( 'post_title', $product->get_parent_id() ); + $title_suffix = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : ''; + + return apply_filters( 'woocommerce_product_variation_title', rtrim( $title_base . $separator . $title_suffix, $separator ), $product, $title_base, $title_suffix ); + } + + /** + * Make sure we store the product version (to track data changes). + * + * @param WC_Product + * @since 3.0.0 + */ + protected function update_version_and_type( &$product ) { + update_post_meta( $product->get_id(), '_product_version', WC_VERSION ); + } + + /** + * Read post data. + * + * @since 3.0.0 + * @param WC_Product + */ + protected function read_product_data( &$product ) { + $id = $product->get_id(); + + $product->set_props( array( + 'description' => get_post_meta( $id, '_variation_description', true ), + 'regular_price' => get_post_meta( $id, '_regular_price', true ), + 'sale_price' => get_post_meta( $id, '_sale_price', true ), + 'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ), + 'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ), + 'manage_stock' => get_post_meta( $id, '_manage_stock', true ), + 'stock_status' => get_post_meta( $id, '_stock_status', true ), + 'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ), + 'virtual' => get_post_meta( $id, '_virtual', true ), + 'downloadable' => get_post_meta( $id, '_downloadable', true ), + 'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ), + 'download_limit' => get_post_meta( $id, '_download_limit', true ), + 'download_expiry' => get_post_meta( $id, '_download_expiry', true ), + 'image_id' => get_post_thumbnail_id( $id ), + 'backorders' => get_post_meta( $id, '_backorders', true ), + 'sku' => get_post_meta( $id, '_sku', true ), + 'stock_quantity' => get_post_meta( $id, '_stock', true ), + 'weight' => get_post_meta( $id, '_weight', true ), + 'length' => get_post_meta( $id, '_length', true ), + 'width' => get_post_meta( $id, '_width', true ), + 'height' => get_post_meta( $id, '_height', true ), + 'tax_class' => ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : get_post_meta( $id, '_tax_class', true ), + ) ); + + if ( $product->is_on_sale( 'edit' ) ) { + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + + $parent_object = get_post( $product->get_parent_id() ); + $terms = get_the_terms( $product->get_parent_id(), 'product_visibility' ); + $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); + $exclude_search = in_array( 'exclude-from-search', $term_names ); + $exclude_catalog = in_array( 'exclude-from-catalog', $term_names ); + + if ( $exclude_search && $exclude_catalog ) { + $catalog_visibility = 'hidden'; + } elseif ( $exclude_search ) { + $catalog_visibility = 'catalog'; + } elseif ( $exclude_catalog ) { + $catalog_visibility = 'search'; + } else { + $catalog_visibility = 'visible'; + } + + $product->set_parent_data( array( + 'title' => $parent_object ? $parent_object->post_title : '', + 'sku' => get_post_meta( $product->get_parent_id(), '_sku', true ), + 'manage_stock' => get_post_meta( $product->get_parent_id(), '_manage_stock', true ), + 'backorders' => get_post_meta( $product->get_parent_id(), '_backorders', true ), + 'stock_quantity' => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ), + 'weight' => get_post_meta( $product->get_parent_id(), '_weight', true ), + 'length' => get_post_meta( $product->get_parent_id(), '_length', true ), + 'width' => get_post_meta( $product->get_parent_id(), '_width', true ), + 'height' => get_post_meta( $product->get_parent_id(), '_height', true ), + 'tax_class' => get_post_meta( $product->get_parent_id(), '_tax_class', true ), + 'shipping_class_id' => absint( current( $this->get_term_ids( $product->get_parent_id(), 'product_shipping_class' ) ) ), + 'image_id' => get_post_thumbnail_id( $product->get_parent_id() ), + 'purchase_note' => get_post_meta( $product->get_parent_id(), '_purchase_note', true ), + 'catalog_visibility' => $catalog_visibility, + ) ); + + // Pull data from the parent when there is no user-facing way to set props. + $product->set_sold_individually( get_post_meta( $product->get_parent_id(), '_sold_individually', true ) ); + $product->set_tax_status( get_post_meta( $product->get_parent_id(), '_tax_status', true ) ); + $product->set_cross_sell_ids( get_post_meta( $product->get_parent_id(), '_crosssell_ids', true ) ); + } + + /** + * For all stored terms in all taxonomies, save them to the DB. + * + * @since 3.0.0 + * @param WC_Product + * @param bool Force update. Used during create. + */ + protected function update_terms( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { + wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); + } + } + + /** + * Update visibility terms based on props. + * + * @since 3.0.0 + * + * @param WC_Product $product + * @param bool $force Force update. Used during create. + */ + protected function update_visibility( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) { + $terms = array(); + + if ( 'outofstock' === $product->get_stock_status() ) { + $terms[] = 'outofstock'; + } + + wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ); + } + } + + /** + * Update attribute meta values. + * + * @since 3.0.0 + * @param WC_Product + * @param bool Force update. Used during create. + */ + protected function update_attributes( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'attributes', $changes ) ) { + global $wpdb; + $attributes = $product->get_attributes(); + $updated_attribute_keys = array(); + foreach ( $attributes as $key => $value ) { + update_post_meta( $product->get_id(), 'attribute_' . $key, $value ); + $updated_attribute_keys[] = 'attribute_' . $key; + } + + // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. + $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d;", $product->get_id() ) ); + + foreach ( $delete_attribute_keys as $key ) { + delete_post_meta( $product->get_id(), $key ); + } + } + } + + /** + * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. + * + * @since 3.0.0 + * @param WC_Product + * @param bool Force update. Used during create. + */ + public function update_post_meta( &$product, $force = false ) { + $meta_key_to_props = array( + '_variation_description' => 'description', + ); + + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + $updated = update_post_meta( $product->get_id(), $meta_key, $value ); + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + parent::update_post_meta( $product, $force ); + } +} diff --git a/includes/data-stores/class-wc-shipping-zone-data-store.php b/includes/data-stores/class-wc-shipping-zone-data-store.php new file mode 100644 index 00000000000..fad0c2a8510 --- /dev/null +++ b/includes/data-stores/class-wc-shipping-zone-data-store.php @@ -0,0 +1,292 @@ +insert( $wpdb->prefix . 'woocommerce_shipping_zones', array( + 'zone_name' => $zone->get_zone_name(), + 'zone_order' => $zone->get_zone_order(), + ) ); + $zone->set_id( $wpdb->insert_id ); + $zone->save_meta_data(); + $this->save_locations( $zone ); + $zone->apply_changes(); + WC_Cache_Helper::incr_cache_prefix( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + } + + /** + * Update zone in the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone + */ + public function update( &$zone ) { + global $wpdb; + if ( $zone->get_id() ) { + $wpdb->update( $wpdb->prefix . 'woocommerce_shipping_zones', array( + 'zone_name' => $zone->get_zone_name(), + 'zone_order' => $zone->get_zone_order(), + ), array( 'zone_id' => $zone->get_id() ) ); + } + $zone->save_meta_data(); + $this->save_locations( $zone ); + $zone->apply_changes(); + WC_Cache_Helper::incr_cache_prefix( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + } + + /** + * Method to read a shipping zone from the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone + * @throws Exception + */ + public function read( &$zone ) { + global $wpdb; + if ( 0 === $zone->get_id() || "0" === $zone->get_id() ) { + $this->read_zone_locations( $zone ); + $zone->set_zone_name( __( 'Rest of the World', 'woocommerce' ) ); + $zone->read_meta_data(); + $zone->set_object_read( true ); + do_action( 'woocommerce_shipping_zone_loaded', $zone ); + } elseif ( $zone_data = $wpdb->get_row( $wpdb->prepare( "SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1;", $zone->get_id() ) ) ) { + $zone->set_zone_name( $zone_data->zone_name ); + $zone->set_zone_order( $zone_data->zone_order ); + $this->read_zone_locations( $zone ); + $zone->read_meta_data(); + $zone->set_object_read( true ); + do_action( 'woocommerce_shipping_zone_loaded', $zone ); + } else { + throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); + } + } + + /** + * Deletes a shipping zone from the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone + * @param array $args Array of args to pass to the delete method. + * @return bool result + */ + public function delete( &$zone, $args = array() ) { + if ( $zone->get_id() ) { + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'zone_id' => $zone->get_id() ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $zone->get_id() ) ); + WC_Cache_Helper::incr_cache_prefix( 'shipping_zones' ); + $id = $zone->get_id(); + $zone->set_id( null ); + WC_Cache_Helper::incr_cache_prefix( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + do_action( 'woocommerce_delete_shipping_zone', $id ); + } + } + + /** + * Get a list of shipping methods for a specific zone. + * + * @since 3.0.0 + * @param int $zone_id Zone ID + * @param bool $enabled_only True to request enabled methods only. + * @return array Array of objects containing method_id, method_order, instance_id, is_enabled + */ + public function get_methods( $zone_id, $enabled_only ) { + global $wpdb; + $raw_methods_sql = $enabled_only ? "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1;" : "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d;"; + return $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $zone_id ) ); + } + + /** + * Get count of methods for a zone. + * + * @since 3.0.0 + * @param int Zone ID + * @return int Method Count + */ + public function get_method_count( $zone_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $zone_id ) ); + } + + /** + * Add a shipping method to a zone. + * + * @since 3.0.0 + * @param int $zone_id Zone ID + * @param string $type Method Type/ID + * @param int $order Method Order + * @return int Instance ID + */ + public function add_method( $zone_id, $type, $order ) { + global $wpdb; + $wpdb->insert( + $wpdb->prefix . 'woocommerce_shipping_zone_methods', + array( + 'method_id' => $type, + 'zone_id' => $zone_id, + 'method_order' => $order, + ), + array( + '%s', + '%d', + '%d', + ) + ); + return $wpdb->insert_id; + } + + /** + * Delete a method instance. + * + * @since 3.0.0 + * @param int $instance_id + */ + public function delete_method( $instance_id ) { + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'instance_id' => $instance_id ) ); + do_action( 'woocommerce_delete_shipping_zone_method', $instance_id ); + } + + /** + * Get a shipping zone method instance. + * + * @since 3.0.0 + * @param int + * @return object + */ + public function get_method( $instance_id ) { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( "SELECT zone_id, method_id, instance_id, method_order, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d LIMIT 1;", $instance_id ) ); + } + + /** + * Find a matching zone ID for a given package. + * + * @since 3.0.0 + * @param object $package + * @return int + */ + public function get_zone_id_from_package( $package ) { + global $wpdb; + + $country = strtoupper( wc_clean( $package['destination']['country'] ) ); + $state = strtoupper( wc_clean( $package['destination']['state'] ) ); + $continent = strtoupper( wc_clean( WC()->countries->get_continent_code_for_country( $country ) ) ); + $postcode = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) ); + + // Work out criteria for our zone search + $criteria = array(); + $criteria[] = $wpdb->prepare( "( ( location_type = 'country' AND location_code = %s )", $country ); + $criteria[] = $wpdb->prepare( "OR ( location_type = 'state' AND location_code = %s )", $country . ':' . $state ); + $criteria[] = $wpdb->prepare( "OR ( location_type = 'continent' AND location_code = %s )", $continent ); + $criteria[] = "OR ( location_type IS NULL ) )"; + + // Postcode range and wildcard matching + $postcode_locations = $wpdb->get_results( "SELECT zone_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_type = 'postcode';" ); + + if ( $postcode_locations ) { + $zone_ids_with_postcode_rules = array_map( 'absint', wp_list_pluck( $postcode_locations, 'zone_id' ) ); + $matches = wc_postcode_location_matcher( $postcode, $postcode_locations, 'zone_id', 'location_code', $country ); + $do_not_match = array_unique( array_diff( $zone_ids_with_postcode_rules, array_keys( $matches ) ) ); + + if ( ! empty( $do_not_match ) ) { + $criteria[] = "AND zones.zone_id NOT IN (" . implode( ',', $do_not_match ) . ")"; + } + } + + // Get matching zones + return $wpdb->get_var( " + SELECT zones.zone_id FROM {$wpdb->prefix}woocommerce_shipping_zones as zones + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_shipping_zone_locations as locations ON zones.zone_id = locations.zone_id AND location_type != 'postcode' + WHERE " . implode( ' ', $criteria ) . " + ORDER BY zone_order ASC LIMIT 1 + " ); + } + + /** + * Return an ordered list of zones. + * + * @since 3.0.0 + * @return array An array of objects containing a zone_id, zone_name, and zone_order. + */ + public function get_zones() { + global $wpdb; + return $wpdb->get_results( "SELECT zone_id, zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones order by zone_order ASC;" ); + } + + + /** + * Return a zone ID from an instance ID. + * + * @since 3.0.0 + * @param int + * @return int + */ + public function get_zone_id_by_instance_id( $id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT zone_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods as methods WHERE methods.instance_id = %d LIMIT 1;", $id ) ); + } + + /** + * Read location data from the database. + * + * @param WC_Shipping_Zone + */ + private function read_zone_locations( &$zone ) { + global $wpdb; + if ( $locations = $wpdb->get_results( $wpdb->prepare( "SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d;", $zone->get_id() ) ) ) { + foreach ( $locations as $location ) { + $zone->add_location( $location->location_code, $location->location_type ); + } + } + } + + /** + * Save locations to the DB. + * This function clears old locations, then re-inserts new if any changes are found. + * + * @since 3.0.0 + * + * @param WC_Shipping_Zone + * + * @return bool|void + */ + private function save_locations( &$zone ) { + $changed_props = array_keys( $zone->get_changes() ); + if ( ! in_array( 'zone_locations', $changed_props ) ) { + return false; + } + + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) ); + + foreach ( $zone->get_zone_locations( 'edit' ) as $location ) { + $wpdb->insert( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( + 'zone_id' => $zone->get_id(), + 'location_code' => $location->code, + 'location_type' => $location->type, + ) ); + } + } +} diff --git a/includes/emails/class-wc-email-cancelled-order.php b/includes/emails/class-wc-email-cancelled-order.php new file mode 100644 index 00000000000..8dbac9969c3 --- /dev/null +++ b/includes/emails/class-wc-email-cancelled-order.php @@ -0,0 +1,175 @@ +id = 'cancelled_order'; + $this->title = __( 'Cancelled order', 'woocommerce' ); + $this->description = __( 'Cancelled order emails are sent to chosen recipient(s) when orders have been marked cancelled (if they were previously processing or on-hold).', 'woocommerce' ); + $this->template_html = 'emails/admin-cancelled-order.php'; + $this->template_plain = 'emails/plain/admin-cancelled-order.php'; + + // Triggers for this email + add_action( 'woocommerce_order_status_processing_to_cancelled_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_on-hold_to_cancelled_notification', array( $this, 'trigger' ), 10, 2 ); + + // Call parent constructor + parent::__construct(); + + // Other settings + $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( '[{site_title}] Cancelled order ({order_number})', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Cancelled order', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order $order Order object. + */ + public function trigger( $order_id, $order = false ) { + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->find['order-date'] = '{order_date}'; + $this->find['order-number'] = '{order_number}'; + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); + $this->replace['order-number'] = $this->object->get_order_number(); + } + + if ( ! $this->is_enabled() || ! $this->get_recipient() ) { + return; + } + + $this->setup_locale(); + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); + } + + /** + * Get content html. + * + * @access public + * @return string + */ + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, + ) ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, + ) ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'recipient' => array( + 'title' => __( 'Recipient(s)', 'woocommerce' ), + 'type' => 'text', + /* translators: %s: admin email */ + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), + 'placeholder' => '', + 'default' => '', + 'desc_tip' => true, + ), + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } +} + +endif; + +return new WC_Email_Cancelled_Order(); diff --git a/includes/emails/class-wc-email-customer-completed-order.php b/includes/emails/class-wc-email-customer-completed-order.php index ed7aed2a4c1..68d499750d9 100644 --- a/includes/emails/class-wc-email-customer-completed-order.php +++ b/includes/emails/class-wc-email-customer-completed-order.php @@ -1,64 +1,64 @@ id = 'customer_completed_order'; - $this->title = __( 'Completed order', 'woocommerce' ); - $this->description = __( 'Order complete emails are sent to the customer when the order is marked complete and usual indicates that the order has been shipped.', 'woocommerce' ); + $this->id = 'customer_completed_order'; + $this->customer_email = true; - $this->heading = __( 'Your order is complete', 'woocommerce' ); - $this->subject = __( 'Your {site_title} order from {order_date} is complete', 'woocommerce' ); + $this->title = __( 'Completed order', 'woocommerce' ); + $this->description = __( 'Order complete emails are sent to customers when their orders are marked completed and usually indicate that their orders have been shipped.', 'woocommerce' ); - $this->template_html = 'emails/customer-completed-order.php'; - $this->template_plain = 'emails/plain/customer-completed-order.php'; + $this->template_html = 'emails/customer-completed-order.php'; + $this->template_plain = 'emails/plain/customer-completed-order.php'; // Triggers for this email - add_action( 'woocommerce_order_status_completed_notification', array( $this, 'trigger' ) ); - - // Other settings - $this->heading_downloadable = $this->get_option( 'heading_downloadable', __( 'Your order is complete - download your files', 'woocommerce' ) ); - $this->subject_downloadable = $this->get_option( 'subject_downloadable', __( 'Your {site_title} order from {order_date} is complete - download your files', 'woocommerce' ) ); + add_action( 'woocommerce_order_status_completed_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constuctor parent::__construct(); } /** - * trigger function. + * Trigger the sending of this email. * - * @access public - * @return void + * @param int $order_id The order ID. + * @param WC_Order $order Order object. */ - function trigger( $order_id ) { + public function trigger( $order_id, $order = false ) { + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } - if ( $order_id ) { - $this->object = get_order( $order_id ); - $this->recipient = $this->object->billing_email; + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); $this->find['order-date'] = '{order_date}'; $this->find['order-number'] = '{order_number}'; - - $this->replace['order-date'] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) ); + + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); $this->replace['order-number'] = $this->object->get_order_number(); } @@ -66,127 +66,104 @@ class WC_Email_Customer_Completed_Order extends WC_Email { return; } + $this->setup_locale(); $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); } /** - * get_subject function. + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} order from {order_date} is complete', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Your order is complete', 'woocommerce' ); + } + + /** + * Get content html. * * @access public * @return string */ - function get_subject() { - if ( ! empty( $this->object ) && $this->object->has_downloadable_item() ) - return apply_filters( 'woocommerce_email_subject_customer_completed_order', $this->format_string( $this->subject_downloadable ), $this->object ); - else - return apply_filters( 'woocommerce_email_subject_customer_completed_order', $this->format_string( $this->subject ), $this->object ); - } - - /** - * get_heading function. - * - * @access public - * @return string - */ - function get_heading() { - if ( ! empty( $this->object ) && $this->object->has_downloadable_item() ) - return apply_filters( 'woocommerce_email_heading_customer_completed_order', $this->format_string( $this->heading_downloadable ), $this->object ); - else - return apply_filters( 'woocommerce_email_heading_customer_completed_order', $this->format_string( $this->heading ), $this->object ); - } - - /** - * get_content_html function. - * - * @access public - * @return string - */ - function get_content_html() { - ob_start(); - wc_get_template( $this->template_html, array( - 'order' => $this->object, + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( + 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => false, - 'plain_text' => false + 'plain_text' => false, + 'email' => $this, ) ); - return ob_get_clean(); } /** - * get_content_plain function. + * Get content plain. * - * @access public * @return string */ - function get_content_plain() { - ob_start(); - wc_get_template( $this->template_plain, array( - 'order' => $this->object, + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => false, - 'plain_text' => true + 'plain_text' => true, + 'email' => $this, ) ); - return ob_get_clean(); } - /** - * Initialise Settings Form Fields - * - * @access public - * @return void - */ - function init_form_fields() { - $this->form_fields = array( + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + $this->form_fields = array( 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable this email notification', 'woocommerce' ), - 'default' => 'yes' + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', ), 'subject' => array( - 'title' => __( 'Subject', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Defaults to %s', 'woocommerce' ), $this->subject ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_subject(), + 'default' => '', ), 'heading' => array( - 'title' => __( 'Email Heading', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Defaults to %s', 'woocommerce' ), $this->heading ), - 'placeholder' => '', - 'default' => '' - ), - 'subject_downloadable' => array( - 'title' => __( 'Subject (downloadable)', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Defaults to %s', 'woocommerce' ), $this->subject_downloadable ), - 'placeholder' => '', - 'default' => '' - ), - 'heading_downloadable' => array( - 'title' => __( 'Email Heading (downloadable)', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Defaults to %s', 'woocommerce' ), $this->heading_downloadable ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_heading(), + 'default' => '', ), 'email_type' => array( - 'title' => __( 'Email type', 'woocommerce' ), - 'type' => 'select', - 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), - 'default' => 'html', - 'class' => 'email_type', - 'options' => array( - 'plain' => __( 'Plain text', 'woocommerce' ), - 'html' => __( 'HTML', 'woocommerce' ), - 'multipart' => __( 'Multipart', 'woocommerce' ), - ) - ) + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), ); - } + } } endif; -return new WC_Email_Customer_Completed_Order(); \ No newline at end of file +return new WC_Email_Customer_Completed_Order(); diff --git a/includes/emails/class-wc-email-customer-invoice.php b/includes/emails/class-wc-email-customer-invoice.php index 0cecb6fc13b..d585621bde5 100644 --- a/includes/emails/class-wc-email-customer-invoice.php +++ b/includes/emails/class-wc-email-customer-invoice.php @@ -1,70 +1,138 @@ id = 'customer_invoice'; - $this->title = __( 'Customer invoice', 'woocommerce' ); - $this->description = __( 'Customer invoice emails can be sent to the user containing order info and payment links.', 'woocommerce' ); + $this->customer_email = true; + $this->title = __( 'Customer invoice', 'woocommerce' ); + $this->description = __( 'Customer invoice emails can be sent to customers containing their order information and payment links.', 'woocommerce' ); $this->template_html = 'emails/customer-invoice.php'; $this->template_plain = 'emails/plain/customer-invoice.php'; - $this->subject = __( 'Invoice for order {order_number} from {order_date}', 'woocommerce'); - $this->heading = __( 'Invoice for order {order_number}', 'woocommerce'); - - $this->subject_paid = __( 'Your {site_title} order from {order_date}', 'woocommerce'); - $this->heading_paid = __( 'Order {order_number} details', 'woocommerce'); - // Call parent constructor parent::__construct(); - $this->heading_paid = $this->get_option( 'heading_paid', $this->heading_paid ); - $this->subject_paid = $this->get_option( 'subject_paid', $this->subject_paid ); + $this->manual = true; } /** - * trigger function. + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject( $paid = false ) { + if ( $paid ) { + return __( 'Your {site_title} order from {order_date}', 'woocommerce' ); + } else { + return __( 'Invoice for order {order_number} from {order_date}', 'woocommerce' ); + } + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading( $paid = false ) { + if ( $paid ) { + return __( 'Order {order_number} details', 'woocommerce' ); + } else { + return __( 'Invoice for order {order_number}', 'woocommerce' ); + } + } + + /** + * Get email subject. * * @access public - * @return void + * @return string */ - function trigger( $order ) { + public function get_subject() { + if ( $this->object->has_status( array( 'completed', 'processing' ) ) ) { + $subject = $this->get_option( 'subject_paid', $this->get_default_subject( true ) ); + $action = 'woocommerce_email_subject_customer_invoice_paid'; + } else { + $subject = $this->get_option( 'subject', $this->get_default_subject() ); + $action = 'woocommerce_email_subject_customer_invoice'; + } + return apply_filters( $action, $this->format_string( $subject ), $this->object ); + } - if ( ! is_object( $order ) ) { - $order = get_order( absint( $order ) ); + /** + * Get email heading. + * + * @access public + * @return string + */ + public function get_heading() { + if ( $this->object->has_status( wc_get_is_paid_statuses() ) ) { + $heading = $this->get_option( 'heading_paid', $this->get_default_heading( true ) ); + $action = 'woocommerce_email_heading_customer_invoice_paid'; + } else { + $heading = $this->get_option( 'heading', $this->get_default_heading() ); + $action = 'woocommerce_email_heading_customer_invoice'; + } + return apply_filters( $action, $this->format_string( $heading ), $this->object ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order $order Order object. + */ + public function trigger( $order_id, $order = false ) { + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); } - if ( $order ) { - $this->object = $order; - $this->recipient = $this->object->billing_email; + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); $this->find['order-date'] = '{order_date}'; $this->find['order-number'] = '{order_number}'; - - $this->replace['order-date'] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) ); + + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); $this->replace['order-number'] = $this->object->get_order_number(); } @@ -72,123 +140,97 @@ class WC_Email_Customer_Invoice extends WC_Email { return; } + $this->setup_locale(); $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); } /** - * get_subject function. + * Get content html. * * @access public * @return string */ - function get_subject() { - if ( $this->object->has_status( array( 'processing', 'completed' ) ) ) { - return apply_filters( 'woocommerce_email_subject_customer_invoice_paid', $this->format_string( $this->subject_paid ), $this->object ); - } else { - return apply_filters( 'woocommerce_email_subject_customer_invoice', $this->format_string( $this->subject ), $this->object ); - } - } - - /** - * get_heading function. - * - * @access public - * @return string - */ - function get_heading() { - if ( $this->object->has_status( array( 'completed', 'processing' ) ) ) { - return apply_filters( 'woocommerce_email_heading_customer_invoice_paid', $this->format_string( $this->heading_paid ), $this->object ); - } else { - return apply_filters( 'woocommerce_email_heading_customer_invoice', $this->format_string( $this->heading ), $this->object ); - } - } - - /** - * get_content_html function. - * - * @access public - * @return string - */ - function get_content_html() { - ob_start(); - wc_get_template( $this->template_html, array( - 'order' => $this->object, + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( + 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => false, - 'plain_text' => false + 'plain_text' => false, + 'email' => $this, ) ); - return ob_get_clean(); } /** - * get_content_plain function. + * Get content plain. * * @access public * @return string */ - function get_content_plain() { - ob_start(); - wc_get_template( $this->template_plain, array( - 'order' => $this->object, + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => false, - 'plain_text' => true + 'plain_text' => true, + 'email' => $this, ) ); - return ob_get_clean(); } - /** - * Initialise Settings Form Fields - * - * @access public - * @return void - */ - function init_form_fields() { - $this->form_fields = array( + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + $this->form_fields = array( 'subject' => array( - 'title' => __( 'Email subject', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Defaults to %s', 'woocommerce' ), $this->subject ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_subject(), + 'default' => '', ), 'heading' => array( - 'title' => __( 'Email heading', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Defaults to %s', 'woocommerce' ), $this->heading ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_heading(), + 'default' => '', ), 'subject_paid' => array( - 'title' => __( 'Email subject (paid)', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Defaults to %s', 'woocommerce' ), $this->subject_paid ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Subject (paid)', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_subject( true ), + 'default' => '', ), 'heading_paid' => array( - 'title' => __( 'Email heading (paid)', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Defaults to %s', 'woocommerce' ), $this->heading_paid ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Email heading (paid)', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_heading( true ), + 'default' => '', ), 'email_type' => array( - 'title' => __( 'Email type', 'woocommerce' ), - 'type' => 'select', - 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), - 'default' => 'html', - 'class' => 'email_type', - 'options' => array( - 'plain' => __( 'Plain text', 'woocommerce' ), - 'html' => __( 'HTML', 'woocommerce' ), - 'multipart' => __( 'Multipart', 'woocommerce' ), - ) - ) + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), ); - } + } } endif; -return new WC_Email_Customer_Invoice(); \ No newline at end of file +return new WC_Email_Customer_Invoice(); diff --git a/includes/emails/class-wc-email-customer-new-account.php b/includes/emails/class-wc-email-customer-new-account.php index a07342d5c05..c9a0ddc25e6 100644 --- a/includes/emails/class-wc-email-customer-new-account.php +++ b/includes/emails/class-wc-email-customer-new-account.php @@ -1,58 +1,101 @@ id = 'customer_new_account'; - $this->title = __( 'New account', 'woocommerce' ); - $this->description = __( 'Customer new account emails are sent when a customer signs up via the checkout or My Account page.', 'woocommerce' ); + /** + * User password. + * + * @var string + */ + public $user_pass; - $this->template_html = 'emails/customer-new-account.php'; - $this->template_plain = 'emails/plain/customer-new-account.php'; + /** + * Is the password generated? + * + * @var bool + */ + public $password_generated; - $this->subject = __( 'Your account on {site_title}', 'woocommerce'); - $this->heading = __( 'Welcome to {site_title}', 'woocommerce'); + /** + * Constructor. + */ + public function __construct() { - // Call parent constuctor + $this->id = 'customer_new_account'; + $this->customer_email = true; + + $this->title = __( 'New account', 'woocommerce' ); + $this->description = __( 'Customer "new account" emails are sent to the customer when a customer signs up via checkout or account pages.', 'woocommerce' ); + + $this->template_html = 'emails/customer-new-account.php'; + $this->template_plain = 'emails/plain/customer-new-account.php'; + + // Call parent constructor parent::__construct(); } /** - * trigger function. + * Get email subject. * - * @access public - * @return void + * @since 3.1.0 + * @return string */ - function trigger( $user_id, $user_pass = '', $password_generated = false ) { + public function get_default_subject() { + return __( 'Your account on {site_title}', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Welcome to {site_title}', 'woocommerce' ); + } + + /** + * Trigger. + * + * @param int $user_id + * @param string $user_pass + * @param bool $password_generated + */ + public function trigger( $user_id, $user_pass = '', $password_generated = false ) { if ( $user_id ) { - $this->object = new WP_User( $user_id ); + $this->object = new WP_User( $user_id ); $this->user_pass = $user_pass; $this->user_login = stripslashes( $this->object->user_login ); @@ -61,53 +104,54 @@ class WC_Email_Customer_New_Account extends WC_Email { $this->password_generated = $password_generated; } - if ( ! $this->is_enabled() || ! $this->get_recipient() ) + if ( ! $this->is_enabled() || ! $this->get_recipient() ) { return; + } + $this->setup_locale(); $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); } /** - * get_content_html function. + * Get content html. * * @access public * @return string */ - function get_content_html() { - ob_start(); - wc_get_template( $this->template_html, array( + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( 'email_heading' => $this->get_heading(), 'user_login' => $this->user_login, 'user_pass' => $this->user_pass, 'blogname' => $this->get_blogname(), 'password_generated' => $this->password_generated, - 'sent_to_admin' => false, - 'plain_text' => false + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, ) ); - return ob_get_clean(); } /** - * get_content_plain function. + * Get content plain. * * @access public * @return string */ - function get_content_plain() { - ob_start(); - wc_get_template( $this->template_plain, array( + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( 'email_heading' => $this->get_heading(), 'user_login' => $this->user_login, 'user_pass' => $this->user_pass, 'blogname' => $this->get_blogname(), 'password_generated' => $this->password_generated, - 'sent_to_admin' => false, - 'plain_text' => true + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, ) ); - return ob_get_clean(); } } endif; -return new WC_Email_Customer_New_Account(); \ No newline at end of file +return new WC_Email_Customer_New_Account(); diff --git a/includes/emails/class-wc-email-customer-note.php b/includes/emails/class-wc-email-customer-note.php index 03e6bc44e63..2c9bc939eec 100644 --- a/includes/emails/class-wc-email-customer-note.php +++ b/includes/emails/class-wc-email-customer-note.php @@ -1,41 +1,44 @@ id = 'customer_note'; - $this->title = __( 'Customer note', 'woocommerce' ); - $this->description = __( 'Customer note emails are sent when you add a note to an order.', 'woocommerce' ); + $this->id = 'customer_note'; + $this->customer_email = true; - $this->template_html = 'emails/customer-note.php'; - $this->template_plain = 'emails/plain/customer-note.php'; + $this->title = __( 'Customer note', 'woocommerce' ); + $this->description = __( 'Customer note emails are sent when you add a note to an order.', 'woocommerce' ); - $this->subject = __( 'Note added to your {site_title} order from {order_date}', 'woocommerce'); - $this->heading = __( 'A note has been added to your order', 'woocommerce'); + $this->template_html = 'emails/customer-note.php'; + $this->template_plain = 'emails/plain/customer-note.php'; // Triggers add_action( 'woocommerce_new_customer_note_notification', array( $this, 'trigger' ) ); @@ -45,79 +48,99 @@ class WC_Email_Customer_Note extends WC_Email { } /** - * trigger function. + * Get email subject. * - * @access public - * @return void + * @since 3.1.0 + * @return string */ - function trigger( $args ) { + public function get_default_subject() { + return __( 'Note added to your {site_title} order from {order_date}', 'woocommerce' ); + } - if ( $args ) { + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'A note has been added to your order', 'woocommerce' ); + } + /** + * Trigger. + * + * @param array $args + */ + public function trigger( $args ) { + if ( ! empty( $args ) ) { $defaults = array( - 'order_id' => '', - 'customer_note' => '' + 'order_id' => '', + 'customer_note' => '', ); $args = wp_parse_args( $args, $defaults ); extract( $args ); - $this->object = get_order( $order_id ); - $this->recipient = $this->object->billing_email; - $this->customer_note = $customer_note; + if ( $order_id && ( $this->object = wc_get_order( $order_id ) ) ) { + $this->recipient = $this->object->get_billing_email(); + $this->customer_note = $customer_note; - $this->find['order-date'] = '{order_date}'; - $this->find['order-number'] = '{order_number}'; - - $this->replace['order-date'] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) ); - $this->replace['order-number'] = $this->object->get_order_number(); + $this->find['order-date'] = '{order_date}'; + $this->find['order-number'] = '{order_number}'; + + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); + $this->replace['order-number'] = $this->object->get_order_number(); + } else { + return; + } } if ( ! $this->is_enabled() || ! $this->get_recipient() ) { return; } + $this->setup_locale(); $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); } /** - * get_content_html function. + * Get content html. * * @access public * @return string */ - function get_content_html() { - ob_start(); - wc_get_template( $this->template_html, array( - 'order' => $this->object, + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( + 'order' => $this->object, 'email_heading' => $this->get_heading(), 'customer_note' => $this->customer_note, 'sent_to_admin' => false, - 'plain_text' => false + 'plain_text' => false, + 'email' => $this, ) ); - return ob_get_clean(); } /** - * get_content_plain function. + * Get content plain. * * @access public * @return string */ - function get_content_plain() { - ob_start(); - wc_get_template( $this->template_plain, array( - 'order' => $this->object, + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, 'email_heading' => $this->get_heading(), 'customer_note' => $this->customer_note, 'sent_to_admin' => false, - 'plain_text' => true + 'plain_text' => true, + 'email' => $this, ) ); - return ob_get_clean(); } } endif; -return new WC_Email_Customer_Note(); \ No newline at end of file +return new WC_Email_Customer_Note(); diff --git a/includes/emails/class-wc-email-customer-on-hold-order.php b/includes/emails/class-wc-email-customer-on-hold-order.php new file mode 100644 index 00000000000..300e8516c00 --- /dev/null +++ b/includes/emails/class-wc-email-customer-on-hold-order.php @@ -0,0 +1,128 @@ +id = 'customer_on_hold_order'; + $this->customer_email = true; + + $this->title = __( 'Order on-hold', 'woocommerce' ); + $this->description = __( 'This is an order notification sent to customers containing order details after an order is placed on-hold.', 'woocommerce' ); + $this->template_html = 'emails/customer-on-hold-order.php'; + $this->template_plain = 'emails/plain/customer-on-hold-order.php'; + + // Triggers for this email + add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + + // Call parent constructor + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} order receipt from {order_date}', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Thank you for your order', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order $order Order object. + */ + public function trigger( $order_id, $order = false ) { + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); + + $this->find['order-date'] = '{order_date}'; + $this->find['order-number'] = '{order_number}'; + + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); + $this->replace['order-number'] = $this->object->get_order_number(); + } + + if ( ! $this->is_enabled() || ! $this->get_recipient() ) { + return; + } + + $this->setup_locale(); + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); + } + + /** + * Get content html. + * + * @access public + * @return string + */ + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) ); + } + + /** + * Get content plain. + * + * @access public + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) ); + } +} + +endif; + +return new WC_Email_Customer_On_Hold_Order(); diff --git a/includes/emails/class-wc-email-customer-processing-order.php b/includes/emails/class-wc-email-customer-processing-order.php index a648c69c27f..85980d9f1f5 100644 --- a/includes/emails/class-wc-email-customer-processing-order.php +++ b/includes/emails/class-wc-email-customer-processing-order.php @@ -1,61 +1,84 @@ id = 'customer_processing_order'; + $this->customer_email = true; - $this->id = 'customer_processing_order'; - $this->title = __( 'Processing order', 'woocommerce' ); - $this->description = __( 'This is an order notification sent to the customer after payment containing order details.', 'woocommerce' ); - - $this->heading = __( 'Thank you for your order', 'woocommerce' ); - $this->subject = __( 'Your {site_title} order receipt from {order_date}', 'woocommerce' ); - - $this->template_html = 'emails/customer-processing-order.php'; - $this->template_plain = 'emails/plain/customer-processing-order.php'; + $this->title = __( 'Processing order', 'woocommerce' ); + $this->description = __( 'This is an order notification sent to customers containing order details after payment.', 'woocommerce' ); + $this->template_html = 'emails/customer-processing-order.php'; + $this->template_plain = 'emails/plain/customer-processing-order.php'; // Triggers for this email - add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ) ); - add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ) ); + add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_on-hold_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor parent::__construct(); } /** - * trigger function. + * Get email subject. * - * @access public - * @return void + * @since 3.1.0 + * @return string */ - function trigger( $order_id ) { + public function get_default_subject() { + return __( 'Your {site_title} order receipt from {order_date}', 'woocommerce' ); + } - if ( $order_id ) { - $this->object = get_order( $order_id ); - $this->recipient = $this->object->billing_email; + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Thank you for your order', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order $order Order object. + */ + public function trigger( $order_id, $order = false ) { + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); $this->find['order-date'] = '{order_date}'; $this->find['order-number'] = '{order_number}'; - - $this->replace['order-date'] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) ); + + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); $this->replace['order-number'] = $this->object->get_order_number(); } @@ -63,44 +86,44 @@ class WC_Email_Customer_Processing_Order extends WC_Email { return; } + $this->setup_locale(); $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); } /** - * get_content_html function. + * Get content html. * * @access public * @return string */ - function get_content_html() { - ob_start(); - wc_get_template( $this->template_html, array( - 'order' => $this->object, - 'email_heading' => $this->get_heading(), - 'sent_to_admin' => false, - 'plain_text' => false - ) ); - return ob_get_clean(); - } - - /** - * get_content_plain function. - * - * @access public - * @return string - */ - function get_content_plain() { - ob_start(); - wc_get_template( $this->template_plain, array( + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => false, - 'plain_text' => true + 'plain_text' => false, + 'email' => $this, + ) ); + } + + /** + * Get content plain. + * + * @access public + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, ) ); - return ob_get_clean(); } } endif; -return new WC_Email_Customer_Processing_Order(); \ No newline at end of file +return new WC_Email_Customer_Processing_Order(); diff --git a/includes/emails/class-wc-email-customer-refunded-order.php b/includes/emails/class-wc-email-customer-refunded-order.php new file mode 100644 index 00000000000..f52223c8b10 --- /dev/null +++ b/includes/emails/class-wc-email-customer-refunded-order.php @@ -0,0 +1,273 @@ +customer_email = true; + $this->id = 'customer_refunded_order'; + $this->title = __( 'Refunded order', 'woocommerce' ); + $this->description = __( 'Order refunded emails are sent to customers when their orders are refunded.', 'woocommerce' ); + $this->template_html = 'emails/customer-refunded-order.php'; + $this->template_plain = 'emails/plain/customer-refunded-order.php'; + + // Triggers for this email + add_action( 'woocommerce_order_fully_refunded_notification', array( $this, 'trigger_full' ), 10, 2 ); + add_action( 'woocommerce_order_partially_refunded_notification', array( $this, 'trigger_partial' ), 10, 2 ); + + // Call parent constuctor + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject( $partial = false ) { + if ( $partial ) { + return __( 'Your {site_title} order from {order_date} has been partially refunded', 'woocommerce' ); + } else { + return __( 'Your {site_title} order from {order_date} has been refunded', 'woocommerce' ); + } + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading( $partial = false ) { + if ( $partial ) { + return __( 'Order {order_number} details', 'woocommerce' ); + } else { + return __( 'Your order has been partially refunded', 'woocommerce' ); + } + } + + /** + * Get email subject. + * + * @access public + * @return string + */ + public function get_subject() { + if ( $this->partial_refund ) { + $subject = $this->get_option( 'subject_partial', $this->get_default_subject( true ) ); + } else { + $subject = $this->get_option( 'subject_full', $this->get_default_heading() ); + } + return apply_filters( 'woocommerce_email_subject_customer_refunded_order', $this->format_string( $subject ), $this->object ); + } + + /** + * Get email heading. + * + * @access public + * @return string + */ + public function get_heading() { + if ( $this->partial_refund ) { + $heading = $this->get_option( 'heading_partial', $this->get_default_heading( true ) ); + } else { + $heading = $this->get_option( 'heading_full', $this->get_default_heading() ); + } + return apply_filters( 'woocommerce_email_heading_customer_refunded_order', $this->format_string( $heading ), $this->object ); + } + + /** + * Set email strings. + * @deprecated 3.1.0 Unused. + */ + public function set_email_strings( $partial_refund = false ) {} + + /** + * Full refund notification. + * + * @param int $order_id + * @param int $refund_id + */ + public function trigger_full( $order_id, $refund_id = null ) { + $this->trigger( $order_id, false, $refund_id ); + } + + /** + * Partial refund notification. + * + * @param int $order_id + * @param int $refund_id + */ + public function trigger_partial( $order_id, $refund_id = null ) { + $this->trigger( $order_id, true, $refund_id ); + } + + /** + * Trigger. + * + * @param int $order_id + * @param bool $partial_refund + * @param int $refund_id + */ + public function trigger( $order_id, $partial_refund = false, $refund_id = null ) { + $this->partial_refund = $partial_refund; + $this->id = $this->partial_refund ? 'customer_partially_refunded_order' : 'customer_refunded_order'; + + if ( $order_id ) { + $this->object = wc_get_order( $order_id ); + $this->recipient = $this->object->get_billing_email(); + + $this->find['order-date'] = '{order_date}'; + $this->find['order-number'] = '{order_number}'; + + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); + $this->replace['order-number'] = $this->object->get_order_number(); + } + + if ( ! empty( $refund_id ) ) { + $this->refund = wc_get_order( $refund_id ); + } else { + $this->refund = false; + } + + if ( ! $this->is_enabled() || ! $this->get_recipient() ) { + return; + } + + $this->setup_locale(); + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); + } + + /** + * Get content html. + * + * @access public + * @return string + */ + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( + 'order' => $this->object, + 'refund' => $this->refund, + 'partial_refund' => $this->partial_refund, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, + 'refund' => $this->refund, + 'partial_refund' => $this->partial_refund, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'subject_full' => array( + 'title' => __( 'Full refund subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'subject_partial' => array( + 'title' => __( 'Partial refund subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_subject( true ), + 'default' => '', + ), + 'heading_full' => array( + 'title' => __( 'Full refund email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'heading_partial' => array( + 'title' => __( 'Partial refund email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_heading( true ), + 'default' => '', + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } +} + +endif; + +return new WC_Email_Customer_Refunded_Order(); diff --git a/includes/emails/class-wc-email-customer-reset-password.php b/includes/emails/class-wc-email-customer-reset-password.php index 228d7a6c558..d7812f60b52 100644 --- a/includes/emails/class-wc-email-customer-reset-password.php +++ b/includes/emails/class-wc-email-customer-reset-password.php @@ -1,48 +1,58 @@ id = 'customer_reset_password'; - $this->title = __( 'Reset password', 'woocommerce' ); - $this->description = __( 'Customer reset password emails are sent when a customer resets their password.', 'woocommerce' ); + /** + * Reset key. + * + * @var string + */ + public $reset_key; - $this->template_html = 'emails/customer-reset-password.php'; - $this->template_plain = 'emails/plain/customer-reset-password.php'; + /** + * Constructor. + */ + public function __construct() { - $this->subject = __( 'Password Reset for {site_title}', 'woocommerce'); - $this->heading = __( 'Password Reset Instructions', 'woocommerce'); + $this->id = 'customer_reset_password'; + $this->customer_email = true; + + $this->title = __( 'Reset password', 'woocommerce' ); + $this->description = __( 'Customer "reset password" emails are sent when customers reset their passwords.', 'woocommerce' ); + + $this->template_html = 'emails/customer-reset-password.php'; + $this->template_plain = 'emails/plain/customer-reset-password.php'; // Trigger add_action( 'woocommerce_reset_password_notification', array( $this, 'trigger' ), 10, 2 ); @@ -52,15 +62,35 @@ class WC_Email_Customer_Reset_Password extends WC_Email { } /** - * trigger function. + * Get email subject. * - * @access public - * @return void + * @since 3.1.0 + * @return string */ - function trigger( $user_login = '', $reset_key = '' ) { + public function get_default_subject() { + return __( 'Password reset for {site_title}', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Password reset instructions', 'woocommerce' ); + } + + /** + * Trigger. + * + * @param string $user_login + * @param string $reset_key + */ + public function trigger( $user_login = '', $reset_key = '' ) { if ( $user_login && $reset_key ) { $this->object = get_user_by( 'login', $user_login ); - + $this->user_login = $user_login; $this->reset_key = $reset_key; $this->user_email = stripslashes( $this->object->user_email ); @@ -71,49 +101,48 @@ class WC_Email_Customer_Reset_Password extends WC_Email { return; } + $this->setup_locale(); $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); - + $this->restore_locale(); } /** - * get_content_html function. + * Get content html. * * @access public * @return string */ - function get_content_html() { - ob_start(); - wc_get_template( $this->template_html, array( + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( 'email_heading' => $this->get_heading(), - 'user_login' => $this->user_login, - 'reset_key' => $this->reset_key, - 'blogname' => $this->get_blogname(), + 'user_login' => $this->user_login, + 'reset_key' => $this->reset_key, + 'blogname' => $this->get_blogname(), 'sent_to_admin' => false, - 'plain_text' => false + 'plain_text' => false, + 'email' => $this, ) ); - return ob_get_clean(); } /** - * get_content_plain function. + * Get content plain. * * @access public * @return string */ - function get_content_plain() { - ob_start(); - wc_get_template( $this->template_plain, array( + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( 'email_heading' => $this->get_heading(), - 'user_login' => $this->user_login, - 'reset_key' => $this->reset_key, - 'blogname' => $this->get_blogname(), + 'user_login' => $this->user_login, + 'reset_key' => $this->reset_key, + 'blogname' => $this->get_blogname(), 'sent_to_admin' => false, - 'plain_text' => true + 'plain_text' => true, + 'email' => $this, ) ); - return ob_get_clean(); } } endif; -return new WC_Email_Customer_Reset_Password(); \ No newline at end of file +return new WC_Email_Customer_Reset_Password(); diff --git a/includes/emails/class-wc-email-failed-order.php b/includes/emails/class-wc-email-failed-order.php new file mode 100644 index 00000000000..0b0ff5927e5 --- /dev/null +++ b/includes/emails/class-wc-email-failed-order.php @@ -0,0 +1,174 @@ +id = 'failed_order'; + $this->title = __( 'Failed order', 'woocommerce' ); + $this->description = __( 'Failed order emails are sent to chosen recipient(s) when orders have been marked failed (if they were previously processing or on-hold).', 'woocommerce' ); + $this->template_html = 'emails/admin-failed-order.php'; + $this->template_plain = 'emails/plain/admin-failed-order.php'; + + // Triggers for this email + add_action( 'woocommerce_order_status_pending_to_failed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_on-hold_to_failed_notification', array( $this, 'trigger' ), 10, 2 ); + + // Call parent constructor + parent::__construct(); + + // Other settings + $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( '[{site_title}] Failed order ({order_number})', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Failed order', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order $order Order object. + */ + public function trigger( $order_id, $order = false ) { + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->find['order-date'] = '{order_date}'; + $this->find['order-number'] = '{order_number}'; + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); + $this->replace['order-number'] = $this->object->get_order_number(); + } + + if ( ! $this->is_enabled() || ! $this->get_recipient() ) { + return; + } + + $this->setup_locale(); + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); + } + + /** + * Get content html. + * + * @access public + * @return string + */ + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, + ) ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, + ) ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'recipient' => array( + 'title' => __( 'Recipient(s)', 'woocommerce' ), + 'type' => 'text', + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), + 'placeholder' => '', + 'default' => '', + 'desc_tip' => true, + ), + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } +} + +endif; + +return new WC_Email_Failed_Order(); diff --git a/includes/emails/class-wc-email-new-order.php b/includes/emails/class-wc-email-new-order.php index 3c7a62f2c4a..911c3f4e7a6 100644 --- a/includes/emails/class-wc-email-new-order.php +++ b/includes/emails/class-wc-email-new-order.php @@ -1,70 +1,85 @@ id = 'new_order'; - $this->title = __( 'New order', 'woocommerce' ); - $this->description = __( 'New order emails are sent when an order is received.', 'woocommerce' ); - - $this->heading = __( 'New customer order', 'woocommerce' ); - $this->subject = __( '[{site_title}] New customer order ({order_number}) - {order_date}', 'woocommerce' ); - - $this->template_html = 'emails/admin-new-order.php'; - $this->template_plain = 'emails/plain/admin-new-order.php'; + public function __construct() { + $this->id = 'new_order'; + $this->title = __( 'New order', 'woocommerce' ); + $this->description = __( 'New order emails are sent to chosen recipient(s) when a new order is received.', 'woocommerce' ); + $this->template_html = 'emails/admin-new-order.php'; + $this->template_plain = 'emails/plain/admin-new-order.php'; // Triggers for this email - add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ) ); - add_action( 'woocommerce_order_status_pending_to_completed_notification', array( $this, 'trigger' ) ); - add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ) ); - add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ) ); - add_action( 'woocommerce_order_status_failed_to_completed_notification', array( $this, 'trigger' ) ); - add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ) ); + add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_pending_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); // Call parent constructor parent::__construct(); // Other settings - $this->recipient = $this->get_option( 'recipient' ); - - if ( ! $this->recipient ) - $this->recipient = get_option( 'admin_email' ); + $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); } /** - * trigger function. + * Get email subject. * - * @access public - * @return void + * @since 3.1.0 + * @return string */ - function trigger( $order_id ) { + public function get_default_subject() { + return __( '[{site_title}] New customer order ({order_number}) - {order_date}', 'woocommerce' ); + } - if ( $order_id ) { - $this->object = get_order( $order_id ); + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'New customer order', 'woocommerce' ); + } + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order $order Order object. + */ + public function trigger( $order_id, $order = false ) { + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; $this->find['order-date'] = '{order_date}'; $this->find['order-number'] = '{order_number}'; - - $this->replace['order-date'] = date_i18n( wc_date_format(), strtotime( $this->object->order_date ) ); + $this->replace['order-date'] = wc_format_datetime( $this->object->get_date_created() ); $this->replace['order-number'] = $this->object->get_order_number(); } @@ -72,94 +87,93 @@ class WC_Email_New_Order extends WC_Email { return; } + $this->setup_locale(); $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + $this->restore_locale(); } /** - * get_content_html function. + * Get content html. * * @access public * @return string */ - function get_content_html() { - ob_start(); - wc_get_template( $this->template_html, array( - 'order' => $this->object, + public function get_content_html() { + return wc_get_template_html( $this->template_html, array( + 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => true, - 'plain_text' => false + 'plain_text' => false, + 'email' => $this, ) ); - return ob_get_clean(); } /** - * get_content_plain function. + * Get content plain. * * @access public * @return string */ - function get_content_plain() { - ob_start(); - wc_get_template( $this->template_plain, array( - 'order' => $this->object, + public function get_content_plain() { + return wc_get_template_html( $this->template_plain, array( + 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => true, - 'plain_text' => true + 'plain_text' => true, + 'email' => $this, ) ); - return ob_get_clean(); } - /** - * Initialise Settings Form Fields - * - * @access public - * @return void - */ - function init_form_fields() { - $this->form_fields = array( + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + $this->form_fields = array( 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable this email notification', 'woocommerce' ), - 'default' => 'yes' + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', ), 'recipient' => array( - 'title' => __( 'Recipient(s)', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), esc_attr( get_option('admin_email') ) ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Recipient(s)', 'woocommerce' ), + 'type' => 'text', + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), + 'placeholder' => '', + 'default' => '', + 'desc_tip' => true, ), 'subject' => array( - 'title' => __( 'Subject', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce' ), $this->subject ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_subject(), + 'default' => '', ), 'heading' => array( - 'title' => __( 'Email Heading', 'woocommerce' ), - 'type' => 'text', - 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce' ), $this->heading ), - 'placeholder' => '', - 'default' => '' + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}, {order_date}, {order_number}' ), + 'placeholder' => $this->get_default_heading(), + 'default' => '', ), 'email_type' => array( - 'title' => __( 'Email type', 'woocommerce' ), - 'type' => 'select', - 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), - 'default' => 'html', - 'class' => 'email_type', - 'options' => array( - 'plain' => __( 'Plain text', 'woocommerce' ), - 'html' => __( 'HTML', 'woocommerce' ), - 'multipart' => __( 'Multipart', 'woocommerce' ), - ) - ) + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), ); - } + } } endif; -return new WC_Email_New_Order(); \ No newline at end of file +return new WC_Email_New_Order(); diff --git a/includes/emails/class-wc-email.php b/includes/emails/class-wc-email.php new file mode 100644 index 00000000000..9de23a39779 --- /dev/null +++ b/includes/emails/class-wc-email.php @@ -0,0 +1,899 @@ +', // Greater-than + '<', // Less-than + '&', // Ampersand + '&', // Ampersand + '&', // Ampersand + '(c)', // Copyright + '(tm)', // Trademark + '(R)', // Registered + '--', // mdash + '-', // ndash + '*', // Bullet + '£', // Pound sign + 'EUR', // Euro sign. € ? + '$', // Dollar sign + '', // Unknown/unhandled entities + ' ', // Runs of spaces, post-handling + ); + + /** + * Constructor. + */ + public function __construct() { + // Init settings + $this->init_form_fields(); + $this->init_settings(); + + // Save settings hook + add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) ); + + // Default template base if not declared in child constructor + if ( is_null( $this->template_base ) ) { + $this->template_base = WC()->plugin_path() . '/templates/'; + } + + // Settings + $this->email_type = $this->get_option( 'email_type' ); + $this->enabled = $this->get_option( 'enabled' ); + + // Find/replace + $this->find['blogname'] = '{blogname}'; + $this->find['site-title'] = '{site_title}'; + $this->replace['blogname'] = $this->get_blogname(); + $this->replace['site-title'] = $this->get_blogname(); + + // For multipart messages + add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) ); + } + + /** + * Handle multipart mail. + * + * @param PHPMailer $mailer + * @return PHPMailer + */ + public function handle_multipart( $mailer ) { + if ( $this->sending && 'multipart' === $this->get_email_type() ) { + $mailer->AltBody = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, strip_tags( $this->get_content_plain() ) ) ); + $this->sending = false; + } + return $mailer; + } + + /** + * Format email string. + * + * @param mixed $string + * @return string + */ + public function format_string( $string ) { + return str_replace( apply_filters( 'woocommerce_email_format_string_find', $this->find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $this->replace, $this ), $string ); + } + + /** + * Set the locale to the store locale for customer emails to make sure emails are in the store language. + */ + public function setup_locale() { + if ( $this->is_customer_email() ) { + wc_switch_to_site_locale(); + } + } + + /** + * Restore the locale to the default locale. Use after finished with setup_locale. + */ + public function restore_locale() { + if ( $this->is_customer_email() ) { + wc_restore_locale(); + } + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + + /** + * Get email subject. + * + * @return string + */ + public function get_subject() { + return apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object ); + } + + /** + * Get email heading. + * + * @return string + */ + public function get_heading() { + return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object ); + } + + /** + * Get valid recipients. + * @return string + */ + public function get_recipient() { + $recipient = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object ); + $recipients = array_map( 'trim', explode( ',', $recipient ) ); + $recipients = array_filter( $recipients, 'is_email' ); + return implode( ', ', $recipients ); + } + + /** + * Get email headers. + * + * @return string + */ + public function get_headers() { + $header = "Content-Type: " . $this->get_content_type() . "\r\n"; + + if ( 'new_order' === $this->id && $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) { + $header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n"; + } + + return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object ); + } + + /** + * Get email attachments. + * + * @return string + */ + public function get_attachments() { + return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object ); + } + + /** + * get_type function. + * + * @return string + */ + public function get_email_type() { + return $this->email_type && class_exists( 'DOMDocument' ) ? $this->email_type : 'plain'; + } + + /** + * Get email content type. + * + * @return string + */ + public function get_content_type() { + switch ( $this->get_email_type() ) { + case 'html' : + return 'text/html'; + case 'multipart' : + return 'multipart/alternative'; + default : + return 'text/plain'; + } + } + + /** + * Return the email's title + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_email_title', $this->title, $this ); + } + + /** + * Return the email's description + * @return string + */ + public function get_description() { + return apply_filters( 'woocommerce_email_description', $this->description, $this ); + } + + /** + * Proxy to parent's get_option and attempt to localize the result using gettext. + * + * @param string $key + * @param mixed $empty_value + * @return string + */ + public function get_option( $key, $empty_value = null ) { + $value = parent::get_option( $key, $empty_value ); + return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value ); + } + + /** + * Checks if this email is enabled and will be sent. + * @return bool + */ + public function is_enabled() { + return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object ); + } + + /** + * Checks if this email is manually sent + * @return bool + */ + public function is_manual() { + return $this->manual; + } + + /** + * Checks if this email is customer focussed. + * @return bool + */ + public function is_customer_email() { + return $this->customer_email; + } + + /** + * Get WordPress blog name. + * + * @return string + */ + public function get_blogname() { + return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + } + + /** + * Get email content. + * + * @return string + */ + public function get_content() { + $this->sending = true; + + if ( 'plain' === $this->get_email_type() ) { + $email_content = preg_replace( $this->plain_search, $this->plain_replace, strip_tags( $this->get_content_plain() ) ); + } else { + $email_content = $this->get_content_html(); + } + + return wordwrap( $email_content, 70 ); + } + + /** + * Apply inline styles to dynamic content. + * + * @param string|null $content + * @return string + */ + public function style_inline( $content ) { + // make sure we only inline CSS for html emails + if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ) ) && class_exists( 'DOMDocument' ) ) { + ob_start(); + wc_get_template( 'emails/email-styles.php' ); + $css = apply_filters( 'woocommerce_email_styles', ob_get_clean() ); + + // apply CSS styles inline for picky email clients + try { + $emogrifier = new Emogrifier( $content, $css ); + $content = $emogrifier->emogrify(); + } catch ( Exception $e ) { + $logger = wc_get_logger(); + $logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) ); + } + } + return $content; + } + + /** + * Get the email content in plain text format. + * @return string + */ + public function get_content_plain() { return ''; } + + /** + * Get the email content in HTML format. + * @return string + */ + public function get_content_html() { return ''; } + + /** + * Get the from name for outgoing emails. + * @return string + */ + public function get_from_name() { + $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this ); + return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES ); + } + + /** + * Get the from address for outgoing emails. + * @return string + */ + public function get_from_address() { + $from_address = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this ); + return sanitize_email( $from_address ); + } + + /** + * Send an email. + * @param string $to + * @param string $subject + * @param string $message + * @param string $headers + * @param string $attachments + * @return bool success + */ + public function send( $to, $subject, $message, $headers, $attachments ) { + add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); + add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); + add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); + + $message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) ); + $return = wp_mail( $to, $subject, $message, $headers, $attachments ); + + remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); + remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); + remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); + + return $return; + } + + /** + * Initialise Settings Form Fields - these are generic email options most will use. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}' ), + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + /* translators: %s: list of placeholders */ + 'description' => sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title}' ), + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + + /** + * Email type options. + * @return array + */ + public function get_email_type_options() { + $types = array( 'plain' => __( 'Plain text', 'woocommerce' ) ); + + if ( class_exists( 'DOMDocument' ) ) { + $types['html'] = __( 'HTML', 'woocommerce' ); + $types['multipart'] = __( 'Multipart', 'woocommerce' ); + } + + return $types; + } + + /** + * Admin Panel Options Processing. + */ + public function process_admin_options() { + // Save regular options + parent::process_admin_options(); + + $post_data = $this->get_post_data(); + + // Save templates + if ( isset( $post_data['template_html_code'] ) ) { + $this->save_template( $post_data['template_html_code'], $this->template_html ); + } + if ( isset( $post_data['template_plain_code'] ) ) { + $this->save_template( $post_data['template_plain_code'], $this->template_plain ); + } + } + + /** + * Get template. + * + * @param string $type + * @return string + */ + public function get_template( $type ) { + $type = basename( $type ); + + if ( 'template_html' === $type ) { + return $this->template_html; + } elseif ( 'template_plain' === $type ) { + return $this->template_plain; + } + return ''; + } + + /** + * Save the email templates. + * + * @since 2.4.0 + * @param string $template_code + * @param string $template_path + */ + protected function save_template( $template_code, $template_path ) { + if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) { + $saved = false; + $file = get_stylesheet_directory() . '/woocommerce/' . $template_path; + $code = wp_unslash( $template_code ); + + if ( is_writeable( $file ) ) { + $f = fopen( $file, 'w+' ); + + if ( false !== $f ) { + fwrite( $f, $code ); + fclose( $f ); + $saved = true; + } + } + + if ( ! $saved ) { + $redirect = add_query_arg( 'wc_error', urlencode( __( 'Could not write to template file.', 'woocommerce' ) ) ); + wp_safe_redirect( $redirect ); + exit; + } + } + } + + /** + * Get the template file in the current theme. + * + * @param string $template + * + * @return string + */ + public function get_theme_template_file( $template ) { + return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template; + } + + /** + * Move template action. + * + * @param string $template_type + */ + protected function move_template_action( $template_type ) { + if ( $template = $this->get_template( $template_type ) ) { + if ( ! empty( $template ) ) { + + $theme_file = $this->get_theme_template_file( $template ); + + if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) { + + // Locate template file + $core_file = $this->template_base . $template; + $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id ); + + // Copy template file + copy( $template_file, $theme_file ); + + /** + * woocommerce_copy_email_template action hook. + * + * @param string $template_type The copied template type + * @param string $email The email object + */ + do_action( 'woocommerce_copy_email_template', $template_type, $this ); + + echo '

    ' . __( 'Template file copied to theme.', 'woocommerce' ) . '

    '; + } + } + } + } + + /** + * Delete template action. + * + * @param string $template_type + */ + protected function delete_template_action( $template_type ) { + if ( $template = $this->get_template( $template_type ) ) { + + if ( ! empty( $template ) ) { + + $theme_file = $this->get_theme_template_file( $template ); + + if ( file_exists( $theme_file ) ) { + unlink( $theme_file ); + + /** + * woocommerce_delete_email_template action hook. + * + * @param string $template The deleted template type + * @param string $email The email object + */ + do_action( 'woocommerce_delete_email_template', $template_type, $this ); + + echo '

    ' . __( 'Template file deleted from theme.', 'woocommerce' ) . '

    '; + } + } + } + } + + /** + * Admin actions. + */ + protected function admin_actions() { + // Handle any actions + if ( + ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) ) + && ( ! empty( $_GET['move_template'] ) || ! empty( $_GET['delete_template'] ) ) + && 'GET' === $_SERVER['REQUEST_METHOD'] + ) { + if ( empty( $_GET['_wc_email_nonce'] ) || ! wp_verify_nonce( $_GET['_wc_email_nonce'], 'woocommerce_email_template_nonce' ) ) { + wp_die( __( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); + } + + if ( ! current_user_can( 'edit_themes' ) ) { + wp_die( __( 'Cheatin’ huh?', 'woocommerce' ) ); + } + + if ( ! empty( $_GET['move_template'] ) ) { + $this->move_template_action( $_GET['move_template'] ); + } + + if ( ! empty( $_GET['delete_template'] ) ) { + $this->delete_template_action( $_GET['delete_template'] ); + } + } + } + + /** + * Admin Options. + * + * Setup the email settings screen. + * Override this in your email. + * + * @since 1.0.0 + */ + public function admin_options() { + // Do admin actions. + $this->admin_actions(); + ?> +

    get_title() ); ?>

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

    + + + +

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

    + + + + + +

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

    + + + + + +

    + + + +
    + +
    + file = trailingslashit( $upload_dir['basedir'] ) . $this->get_filename(); + $this->column_names = $this->get_default_column_names(); + } + + /** + * Get the file contents. + * + * @since 3.1.0 + * @return string + */ + public function get_file() { + $file = ''; + if ( @file_exists( $this->file ) ) { + $file = @file_get_contents( $this->file ); + } else { + @file_put_contents( $this->file, '' ); + @chmod( $this->file, 0664 ); + } + return $file; + } + + /** + * Serve the file and remove once sent to the client. + * + * @since 3.1.0 + */ + public function export() { + $this->send_headers(); + $this->send_content( $this->get_file() ); + @unlink( $this->file ); + die(); + } + + /** + * Generate the CSV file. + * + * @since 3.1.0 + */ + public function generate_file() { + if ( 1 === $this->get_page() ) { + @unlink( $this->file ); + } + $this->prepare_data_to_export(); + $this->write_csv_data( $this->get_csv_data() ); + } + + /** + * Write data to the file. + * + * @since 3.1.0 + * @param string $data + */ + protected function write_csv_data( $data ) { + $file = $this->get_file(); + + // Add columns when finished. + if ( 100 === $this->get_percent_complete() ) { + $file = chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers() . $file; + } + + $file .= $data; + @file_put_contents( $this->file, $file ); + } + + /** + * Get page. + * + * @since 3.1.0 + * @return int + */ + public function get_page() { + return $this->page; + } + + /** + * Set page. + * + * @since 3.1.0 + * @param int $page + */ + public function set_page( $page ) { + $this->page = absint( $page ); + } + + /** + * Get count of records exported. + * + * @since 3.1.0 + * @return int + */ + public function get_total_exported() { + return ( $this->get_page() * $this->get_limit() ) + $this->exported_row_count; + } + + /** + * Get total % complete. + * + * @since 3.1.0 + * @return int + */ + public function get_percent_complete() { + return $this->total_rows ? floor( ( $this->get_total_exported() / $this->total_rows ) * 100 ) : 100; + } +} diff --git a/includes/export/abstract-wc-csv-exporter.php b/includes/export/abstract-wc-csv-exporter.php new file mode 100644 index 00000000000..978df5e2273 --- /dev/null +++ b/includes/export/abstract-wc-csv-exporter.php @@ -0,0 +1,410 @@ +export_type}_export_column_names", $this->column_names, $this ); + } + + /** + * Set column names. + * + * @since 3.1.0 + * @param array $column_names + */ + public function set_column_names( $column_names ) { + $this->column_names = array(); + + foreach ( $column_names as $column_id => $column_name ) { + $this->column_names[ wc_clean( $column_id ) ] = wc_clean( $column_name ); + } + } + + /** + * Return an array of columns to export. + * + * @since 3.1.0 + * @return array + */ + public function get_columns_to_export() { + return $this->columns_to_export; + } + + /** + * Set columns to export. + * + * @since 3.1.0 + * @param array $column_names + */ + public function set_columns_to_export( $columns ) { + $this->columns_to_export = array_map( 'wc_clean', $columns ); + } + + /** + * See if a column is to be exported or not. + * + * @since 3.1.0 + * @param string $column_id + * @return boolean + */ + public function is_column_exporting( $column_id ) { + $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id; + $columns_to_export = $this->get_columns_to_export(); + + if ( empty( $columns_to_export ) ) { + return true; + } + + if ( in_array( $column_id, $columns_to_export ) ) { + return true; + } + + return false; + } + + /** + * Return default columns. + * + * @since 3.1.0 + * @return array + */ + public function get_default_column_names() { + return array(); + } + + /** + * Do the export. + * + * @since 3.1.0 + */ + public function export() { + $this->prepare_data_to_export(); + $this->send_headers(); + $this->send_content( chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers() . $this->get_csv_data() ); + die(); + } + + /** + * Set the export headers. + * + * @since 3.1.0 + */ + public function send_headers() { + if ( function_exists( 'gc_enable' ) ) { + gc_enable(); + } + if ( function_exists( 'apache_setenv' ) ) { + @apache_setenv( 'no-gzip', 1 ); + } + @ini_set( 'zlib.output_compression', 'Off' ); + @ini_set( 'output_buffering', 'Off' ); + @ini_set( 'output_handler', '' ); + ignore_user_abort( true ); + wc_set_time_limit( 0 ); + nocache_headers(); + header( 'Content-Type: text/csv; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename=' . $this->get_filename() ); + header( 'Pragma: no-cache' ); + header( 'Expires: 0' ); + } + + /** + * Generate and return a filename. + * + * @return string + */ + public function get_filename() { + return sanitize_file_name( 'wc-' . $this->export_type . '-export-' . date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) . '.csv' ); + } + + /** + * Set the export content. + * + * @since 3.1.0 + */ + public function send_content( $csv_data ) { + echo $csv_data; + } + + /** + * Get CSV data for this export. + * + * @since 3.1.0 + * @return string + */ + protected function get_csv_data() { + return $this->export_rows(); + } + + /** + * Export column headers in CSV format. + * + * @since 3.1.0 + * @return string + */ + protected function export_column_headers() { + $columns = $this->get_column_names(); + $export_row = array(); + $buffer = fopen( 'php://output', 'w' ); + ob_start(); + + foreach ( $columns as $column_id => $column_name ) { + if ( ! $this->is_column_exporting( $column_id ) ) { + continue; + } + $export_row[] = $column_name; + } + + fputcsv( $buffer, $export_row ); + + return ob_get_clean(); + } + + /** + * Get data that will be exported. + * + * @since 3.1.0 + * @return array + */ + protected function get_data_to_export() { + return $this->row_data; + } + + /** + * Export rows in CSV format. + * + * @since 3.1.0 + * @return array + */ + protected function export_rows() { + $data = $this->get_data_to_export(); + $buffer = fopen( 'php://output', 'w' ); + ob_start(); + + array_walk( $data, array( $this, 'export_row' ), $buffer ); + + return apply_filters( "woocommerce_{$this->export_type}_export_rows", ob_get_clean(), $this ); + } + + /** + * Export rows to an array ready for the CSV. + * + * @since 3.1.0 + * @param array $row_data + */ + protected function export_row( $row_data, $key, $buffer ) { + $columns = $this->get_column_names(); + $export_row = array(); + + foreach ( $columns as $column_id => $column_name ) { + if ( ! $this->is_column_exporting( $column_id ) ) { + continue; + } + if ( isset( $row_data[ $column_id ] ) ) { + $export_row[] = $this->format_data( $row_data[ $column_id ] ); + } else { + $export_row[] = ''; + } + } + + fputcsv( $buffer, $export_row ); + ++ $this->exported_row_count; + } + + /** + * Get batch limit. + * + * @since 3.1.0 + * @return int + */ + public function get_limit() { + return $this->limit; + } + + /** + * Set batch limit. + * + * @since 3.1.0 + * @param int $limit + */ + public function set_limit( $limit ) { + $this->limit = absint( $limit ); + } + + /** + * Get count of records exported. + * + * @since 3.1.0 + * @return int + */ + public function get_total_exported() { + return $this->exported_row_count; + } + + /** + * Escape a string to be used in a CSV context + * + * Malicious input can inject formulas into CSV files, opening up the possibility + * for phishing attacks and disclosure of sensitive information. + * + * Additionally, Excel exposes the ability to launch arbitrary commands through + * the DDE protocol. + * + * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/ + * @see https://hackerone.com/reports/72785 + * + * @since 3.1.0 + * @param string $field CSV field to escape + * @return string + */ + public function escape_data( $data ) { + $active_content_triggers = array( '=', '+', '-', '@' ); + + if ( in_array( mb_substr( $data, 0, 1 ), $active_content_triggers, true ) ) { + $data = "'" . $data; + } + + return $data; + } + + /** + * Format and escape data ready for the CSV file. + * + * @since 3.1.0 + * @param string $data + * @return string + */ + public function format_data( $data ) { + if ( ! is_scalar( $data ) ) { + if ( is_a( $data, 'WC_Datetime' ) ) { + $data = $data->date( 'Y-m-d G:i:s' ); + } else { + $data = ''; // Not supported. + } + } elseif ( is_bool( $data ) ) { + $data = $data ? 1 : 0; + } + + $data = (string) urldecode( $data ); + $encoding = mb_detect_encoding( $data, 'UTF-8, ISO-8859-1', true ); + $data = 'UTF-8' === $encoding ? $data : utf8_encode( $data ); + return $this->escape_data( $data ); + } + + /** + * Format term ids to names. + * + * @since 3.1.0 + * @param array $term_ids + * @param string $taxonomy + * @return array + */ + public function format_term_ids( $term_ids, $taxonomy ) { + $term_ids = wp_parse_id_list( $term_ids ); + + if ( ! count( $term_ids ) ) { + return ''; + } + + $formatted_terms = array(); + + if ( is_taxonomy_hierarchical( $taxonomy ) ) { + foreach ( $term_ids as $term_id ) { + $formatted_term = array(); + $ancestor_ids = array_reverse( get_ancestors( $term_id, $taxonomy ) ); + + foreach ( $ancestor_ids as $ancestor_id ) { + $term = get_term( $ancestor_id, $taxonomy ); + if ( $term && ! is_wp_error( $term ) ) { + $formatted_term[] = $term->name; + } + } + + $term = get_term( $term_id, $taxonomy ); + + if ( $term && ! is_wp_error( $term ) ) { + $formatted_term[] = $term->name; + } + + $formatted_terms[] = implode( ' > ', $formatted_term ); + } + } else { + foreach ( $term_ids as $term_id ) { + $term = get_term( $term_id, $taxonomy ); + + if ( $term && ! is_wp_error( $term ) ) { + $formatted_terms[] = $term->name; + } + } + } + + return implode( ', ', $formatted_terms ); + } +} diff --git a/includes/export/class-wc-product-csv-exporter.php b/includes/export/class-wc-product-csv-exporter.php new file mode 100644 index 00000000000..010de911b12 --- /dev/null +++ b/includes/export/class-wc-product-csv-exporter.php @@ -0,0 +1,547 @@ +set_product_types_to_export( array_merge( array_keys( wc_get_product_types() ), array( 'variation' ) ) ); + } + + /** + * Should meta be exported? + * + * @since 3.1.0 + * @param bool $enable_meta_export + */ + public function enable_meta_export( $enable_meta_export ) { + $this->enable_meta_export = (bool) $enable_meta_export; + } + + /** + * Product types to export. + * + * @since 3.1.0 + * @param array $product_types_to_export + */ + public function set_product_types_to_export( $product_types_to_export ) { + $this->product_types_to_export = array_map( 'wc_clean', $product_types_to_export ); + } + + /** + * Return an array of columns to export. + * + * @since 3.1.0 + * @return array + */ + public function get_default_column_names() { + return apply_filters( "woocommerce_product_export_{$this->export_type}_default_columns", array( + 'id' => __( 'ID', 'woocommerce' ), + 'type' => __( 'Type', 'woocommerce' ), + 'sku' => __( 'SKU', 'woocommerce' ), + 'name' => __( 'Name', 'woocommerce' ), + 'published' => __( 'Published', 'woocommerce' ), + 'featured' => __( 'Is featured?', 'woocommerce' ), + 'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ), + 'short_description' => __( 'Short description', 'woocommerce' ), + 'description' => __( 'Description', 'woocommerce' ), + 'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ), + 'date_on_sale_to' => __( 'Date sale price ends', 'woocommerce' ), + 'tax_status' => __( 'Tax status', 'woocommerce' ), + 'tax_class' => __( 'Tax class', 'woocommerce' ), + 'stock_status' => __( 'In stock?', 'woocommerce' ), + 'stock' => __( 'Stock', 'woocommerce' ), + 'backorders' => __( 'Backorders allowed?', 'woocommerce' ), + 'sold_individually' => __( 'Sold individually?', 'woocommerce' ), + 'weight' => sprintf( __( 'Weight (%s)', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), + 'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'width' => sprintf( __( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'reviews_allowed' => __( 'Allow customer reviews?', 'woocommerce' ), + 'purchase_note' => __( 'Purchase note', 'woocommerce' ), + 'sale_price' => __( 'Sale price', 'woocommerce' ), + 'regular_price' => __( 'Regular price', 'woocommerce' ), + 'category_ids' => __( 'Categories', 'woocommerce' ), + 'tag_ids' => __( 'Tags', 'woocommerce' ), + 'shipping_class_id' => __( 'Shipping class', 'woocommerce' ), + 'images' => __( 'Images', 'woocommerce' ), + 'download_limit' => __( 'Download limit', 'woocommerce' ), + 'download_expiry' => __( 'Download expiry days', 'woocommerce' ), + 'parent_id' => __( 'Parent', 'woocommerce' ), + 'grouped_products' => __( 'Grouped products', 'woocommerce' ), + 'upsell_ids' => __( 'Upsells', 'woocommerce' ), + 'cross_sell_ids' => __( 'Cross-sells', 'woocommerce' ), + 'product_url' => __( 'External URL', 'woocommerce' ), + 'button_text' => __( 'Button text', 'woocommerce' ), + ) ); + } + + /** + * Prepare data for export. + * + * @since 3.1.0 + */ + public function prepare_data_to_export() { + $columns = $this->get_column_names(); + $products = wc_get_products( array( + 'status' => array( 'private', 'publish' ), + 'type' => $this->product_types_to_export, + 'limit' => $this->get_limit(), + 'page' => $this->get_page(), + 'orderby' => array( + 'ID' => 'ASC', + ), + 'return' => 'objects', + 'paginate' => true, + ) ); + + $this->total_rows = $products->total; + $this->row_data = array(); + + foreach ( $products->products as $product ) { + $row = array(); + foreach ( $columns as $column_id => $column_name ) { + $column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id; + $value = ''; + + // Skip some columns if dynamically handled later or if we're being selective. + if ( in_array( $column_id, array( 'downloads', 'attributes', 'meta' ) ) || ! $this->is_column_exporting( $column_id ) ) { + continue; + } + + // Filter for 3rd parties. + if ( has_filter( "woocommerce_product_export_{$this->export_type}_column_{$column_id}" ) ) { + $value = apply_filters( "woocommerce_product_export_{$this->export_type}_column_{$column_id}", '', $product ); + + // Handle special columns which don't map 1:1 to product data. + } elseif ( is_callable( array( $this, "get_column_value_{$column_id}" ) ) ) { + $value = $this->{"get_column_value_{$column_id}"}( $product ); + + // Default and custom handling. + } elseif ( is_callable( array( $product, "get_{$column_id}" ) ) ) { + $value = $product->{"get_{$column_id}"}( 'edit' ); + } + + $row[ $column_id ] = $value; + } + + $this->prepare_downloads_for_export( $product, $row ); + $this->prepare_attributes_for_export( $product, $row ); + $this->prepare_meta_for_export( $product, $row ); + + $this->row_data[] = apply_filters( 'woocommerce_product_export_row_data', $row, $product ); + } + } + + /** + * Get published value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return int + */ + protected function get_column_value_published( $product ) { + return 'publish' === $product->get_status( 'edit' ) ? 1 : 0; + } + + /** + * Get product_cat value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_category_ids( $product ) { + $term_ids = $product->get_category_ids( 'edit' ); + return $this->format_term_ids( $term_ids, 'product_cat' ); + } + + /** + * Get product_tag value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_tag_ids( $product ) { + $term_ids = $product->get_tag_ids( 'edit' ); + return $this->format_term_ids( $term_ids, 'product_tag' ); + } + + /** + * Get product_shipping_class value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_shipping_class_id( $product ) { + $term_ids = $product->get_shipping_class_id( 'edit' ); + return $this->format_term_ids( $term_ids, 'product_shipping_class' ); + } + + /** + * Get images value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_images( $product ) { + $image_ids = array_merge( array( $product->get_image_id( 'edit' ) ), $product->get_gallery_image_ids( 'edit' ) ); + $images = array(); + + foreach ( $image_ids as $image_id ) { + $image = wp_get_attachment_image_src( $image_id, 'full' ); + + if ( $image ) { + $images[] = $image[0]; + } + } + + return implode( ', ', $images ); + } + + /** + * Prepare linked products for export. + * + * @since 3.1.0 + * @param int[] $linked_products + * @return string + */ + protected function prepare_linked_products_for_export( $linked_products ) { + $product_list = array(); + + foreach ( $linked_products as $linked_product ) { + if ( $linked_product->get_sku() ) { + $product_list[] = $linked_product->get_sku(); + } else { + $product_list[] = 'id:' . $linked_product->get_id(); + } + } + + return implode( ',', $product_list ); + } + + /** + * Get cross_sell_ids value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_cross_sell_ids( $product ) { + return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_cross_sell_ids( 'edit' ) ) ) ); + } + + /** + * Get upsell_ids value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_upsell_ids( $product ) { + return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_upsell_ids( 'edit' ) ) ) ); + } + + /** + * Get parent_id value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_parent_id( $product ) { + if ( $product->get_parent_id( 'edit' ) ) { + $parent = wc_get_product( $product->get_parent_id( 'edit' ) ); + if ( ! $parent ) { + return ''; + } + + return $parent->get_sku( 'edit' ) ? $parent->get_sku( 'edit' ) : 'id:' . $parent->get_id(); + } + return ''; + } + + /** + * Get grouped_products value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_grouped_products( $product ) { + if ( 'grouped' !== $product->get_type() ) { + return ''; + } + + $grouped_products = array(); + $child_ids = $product->get_children( 'edit' ); + foreach ( $child_ids as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! $child ) { + continue; + } + + $grouped_products[] = $child->get_sku( 'edit' ) ? $child->get_sku( 'edit' ) : 'id:' . $child_id; + } + return implode( ',', $grouped_products ); + } + + /** + * Get download_limit value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_download_limit( $product ) { + return $product->is_downloadable() && $product->get_download_limit( 'edit' ) ? $product->get_download_limit( 'edit' ) : ''; + } + + /** + * Get download_expiry value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_download_expiry( $product ) { + return $product->is_downloadable() && $product->get_download_expiry( 'edit' ) ? $product->get_download_expiry( 'edit' ) : ''; + } + + /** + * Get stock value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_stock( $product ) { + $manage_stock = $product->get_manage_stock( 'edit' ); + $stock_quantity = $product->get_stock_quantity( 'edit' ); + + if ( $product->is_type( 'variation' && 'parent' === $manage_stock ) ) { + return 'parent'; + } elseif ( $manage_stock ) { + return $stock_quantity; + } else { + return ''; + } + } + + /** + * Get stock status value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_stock_status( $product ) { + $status = $product->get_stock_status( 'edit' ); + return 'instock' === $status ? 1 : 0; + } + + /** + * Get backorders. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_backorders( $product ) { + $backorders = $product->get_backorders( 'edit' ); + + switch ( $backorders ) { + case 'notify' : + return 'notify'; + default : + return wc_string_to_bool( $backorders ) ? 1 : 0; + } + } + + /** + * Get type value. + * + * @since 3.1.0 + * @param WC_Product $product + * @return string + */ + protected function get_column_value_type( $product ) { + $types = array(); + $types[] = $product->get_type(); + + if ( $product->is_downloadable() ) { + $types[] = 'downloadable'; + } + + if ( $product->is_virtual() ) { + $types[] = 'virtual'; + } + + return implode( ', ', $types ); + } + + /** + * Export downloads. + * + * @since 3.1.0 + * @param WC_Product $product + * @param array $row + */ + protected function prepare_downloads_for_export( $product, &$row ) { + if ( $product->is_downloadable() && $this->is_column_exporting( 'downloads' ) ) { + $downloads = $product->get_downloads( 'edit' ); + + if ( $downloads ) { + $i = 1; + foreach ( $downloads as $download ) { + $this->column_names[ 'downloads:name' . $i ] = sprintf( __( 'Download %d name', 'woocommerce' ), $i ); + $this->column_names[ 'downloads:url' . $i ] = sprintf( __( 'Download %d URL', 'woocommerce' ), $i ); + $row[ 'downloads:name' . $i ] = $download->get_name(); + $row[ 'downloads:url' . $i ] = $download->get_file(); + $i++; + } + } + } + } + + /** + * Export attributes data. + * + * @since 3.1.0 + * @param WC_Product $product + * @param array $row + */ + protected function prepare_attributes_for_export( $product, &$row ) { + if ( $this->is_column_exporting( 'attributes' ) ) { + $attributes = $product->get_attributes(); + $default_attributes = $product->get_default_attributes(); + + if ( count( $attributes ) ) { + $i = 1; + foreach ( $attributes as $attribute_name => $attribute ) { + $this->column_names[ 'attributes:name' . $i ] = sprintf( __( 'Attribute %d name', 'woocommerce' ), $i ); + $this->column_names[ 'attributes:value' . $i ] = sprintf( __( 'Attribute %d value(s)', 'woocommerce' ), $i ); + $this->column_names[ 'attributes:visible' . $i ] = sprintf( __( 'Attribute %d visible', 'woocommerce' ), $i ); + $this->column_names[ 'attributes:taxonomy' . $i ] = sprintf( __( 'Attribute %d global', 'woocommerce' ), $i ); + + if ( is_a( $attribute, 'WC_Product_Attribute' ) ) { + $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute->get_name(), $product ); + + if ( $attribute->is_taxonomy() ) { + $terms = $attribute->get_terms(); + $values = array(); + + foreach ( $terms as $term ) { + $values[] = $term->name; + } + + $row[ 'attributes:value' . $i ] = implode( ', ', $values ); + $row[ 'attributes:taxonomy' . $i ] = 1; + } else { + $row[ 'attributes:value' . $i ] = implode( ', ', $attribute->get_options() ); + $row[ 'attributes:taxonomy' . $i ] = 0; + } + + $row[ 'attributes:visible' . $i ] = $attribute->get_visible(); + } else { + $row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute_name, $product ); + + if ( 0 === strpos( $attribute_name, 'pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $attribute_name ); + $row[ 'attributes:value' . $i ] = $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute; + $row[ 'attributes:taxonomy' . $i ] = 1; + } else { + $row[ 'attributes:value' . $i ] = $attribute; + $row[ 'attributes:taxonomy' . $i ] = 0; + } + + $row[ 'attributes:visible' . $i ] = ''; + } + + if ( $product->is_type( 'variable' ) && isset( $default_attributes[ sanitize_title( $attribute_name ) ] ) ) { + $this->column_names[ 'attributes:default' . $i ] = sprintf( __( 'Attribute %d default', 'woocommerce' ), $i ); + $default_value = $default_attributes[ sanitize_title( $attribute_name ) ]; + + if ( 0 === strpos( $attribute_name, 'pa_' ) ) { + $option_term = get_term_by( 'slug', $default_value, $attribute_name ); + $row[ 'attributes:default' . $i ] = $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $default_value; + } else { + $row[ 'attributes:default' . $i ] = $default_value; + } + } + $i++; + } + } + } + } + + /** + * Export meta data. + * + * @since 3.1.0 + * @param WC_Product $product + * @param array $row + */ + protected function prepare_meta_for_export( $product, &$row ) { + if ( $this->enable_meta_export ) { + $meta_data = $product->get_meta_data(); + + if ( count( $meta_data ) ) { + $i = 1; + foreach ( $meta_data as $meta ) { + if ( ! is_scalar( $meta->value ) ) { + continue; + } + $column_key = 'meta:' . esc_attr( $meta->key ); + $this->column_names[ $column_key ] = sprintf( __( 'Meta: %s', 'woocommerce' ), $meta->key ); + $row[ $column_key ] = $meta->value; + $i++; + } + } + } + } +} diff --git a/includes/gateways/bacs/class-wc-gateway-bacs.php b/includes/gateways/bacs/class-wc-gateway-bacs.php index dabeab20fdb..57df990dce5 100644 --- a/includes/gateways/bacs/class-wc-gateway-bacs.php +++ b/includes/gateways/bacs/class-wc-gateway-bacs.php @@ -1,26 +1,32 @@ id = 'bacs'; - $this->icon = apply_filters('woocommerce_bacs_icon', ''); + $this->icon = apply_filters( 'woocommerce_bacs_icon', '' ); $this->has_fields = false; $this->method_title = __( 'BACS', 'woocommerce' ); $this->method_description = __( 'Allows payments by BACS, more commonly known as direct bank/wire transfer.', 'woocommerce' ); @@ -29,10 +35,10 @@ class WC_Gateway_BACS extends WC_Payment_Gateway { $this->init_form_fields(); $this->init_settings(); - // Define user set variables + // Define user set variables $this->title = $this->get_option( 'title' ); $this->description = $this->get_option( 'description' ); - $this->instructions = $this->get_option( 'instructions', $this->description ); + $this->instructions = $this->get_option( 'instructions' ); // BACS account fields shown on the thanks page and in emails $this->account_details = get_option( 'woocommerce_bacs_accounts', @@ -43,43 +49,44 @@ class WC_Gateway_BACS extends WC_Payment_Gateway { 'sort_code' => $this->get_option( 'sort_code' ), 'bank_name' => $this->get_option( 'bank_name' ), 'iban' => $this->get_option( 'iban' ), - 'bic' => $this->get_option( 'bic' ) - ) + 'bic' => $this->get_option( 'bic' ), + ), ) ); // Actions add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'save_account_details' ) ); - add_action( 'woocommerce_thankyou_bacs', array( $this, 'thankyou_page' ) ); + add_action( 'woocommerce_thankyou_bacs', array( $this, 'thankyou_page' ) ); - // Customer Emails - add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); - } + // Customer Emails + add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); + } - /** - * Initialise Gateway Settings Form Fields - */ - public function init_form_fields() { - $this->form_fields = array( + /** + * Initialise Gateway Settings Form Fields. + */ + public function init_form_fields() { + + $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', - 'label' => __( 'Enable Bank Transfer', 'woocommerce' ), - 'default' => 'yes' + 'label' => __( 'Enable bank transfer', 'woocommerce' ), + 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), - 'default' => __( 'Direct Bank Transfer', 'woocommerce' ), + 'default' => __( 'Direct bank transfer', 'woocommerce' ), 'desc_tip' => true, ), 'description' => array( 'title' => __( 'Description', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), - 'default' => __( 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order won\'t be shipped until the funds have cleared in our account.', 'woocommerce' ), + 'default' => __( 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', 'woocommerce' ), 'desc_tip' => true, ), 'instructions' => array( @@ -90,91 +97,104 @@ class WC_Gateway_BACS extends WC_Payment_Gateway { 'desc_tip' => true, ), 'account_details' => array( - 'type' => 'account_details' + 'type' => 'account_details', ), ); - } - /** - * generate_account_details_html function. - */ - public function generate_account_details_html() { - ob_start(); - ?> - - : - - - - - - - - - - - - - - - account_details ) { - foreach ( $this->account_details as $account ) { - $i++; + } - echo ' - - - - - - - - '; - } - } - ?> - - - - - - -
     
    - - - - + + $account_names[ $i ], + $accounts[] = array( + 'account_name' => $account_names[ $i ], 'account_number' => $account_numbers[ $i ], 'bank_name' => $bank_names[ $i ], 'sort_code' => $sort_codes[ $i ], 'iban' => $ibans[ $i ], - 'bic' => $bics[ $i ] - ); - } - } + 'bic' => $bics[ $i ], + ); + } + } - update_option( 'woocommerce_bacs_accounts', $accounts ); - } + update_option( 'woocommerce_bacs_accounts', $accounts ); + + } + + /** + * Output for the order received page. + * + * @param int $order_id + */ + public function thankyou_page( $order_id ) { - /** - * Output for the order received page. - */ - public function thankyou_page( $order_id ) { if ( $this->instructions ) { - echo wpautop( wptexturize( wp_kses_post( $this->instructions ) ) ); - } - $this->bank_details( $order_id ); - } + echo wpautop( wptexturize( wp_kses_post( $this->instructions ) ) ); + } + $this->bank_details( $order_id ); - /** - * Add content to the WC emails. - * - * @access public - * @param WC_Order $order - * @param bool $sent_to_admin - * @param bool $plain_text - * @return void - */ - public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { - if ( ! $sent_to_admin && 'bacs' === $order->payment_method && $order->has_status( 'on-hold' ) ) { + } + + /** + * Add content to the WC emails. + * + * @param WC_Order $order + * @param bool $sent_to_admin + * @param bool $plain_text + */ + public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { + + if ( ! $sent_to_admin && 'bacs' === $order->get_payment_method() && $order->has_status( 'on-hold' ) ) { if ( $this->instructions ) { echo wpautop( wptexturize( $this->instructions ) ) . PHP_EOL; } - $this->bank_details( $order->id ); + $this->bank_details( $order->get_id() ); } - } - /** - * Get bank details and place into a list format - */ - private function bank_details( $order_id = '' ) { - if ( empty( $this->account_details ) ) { - return; - } + } - echo '

    ' . __( 'Our Bank Details', 'woocommerce' ) . '

    ' . PHP_EOL; + /** + * Get bank details and place into a list format. + * + * @param int $order_id + */ + private function bank_details( $order_id = '' ) { - $bacs_accounts = apply_filters( 'woocommerce_bacs_accounts', $this->account_details ); + if ( empty( $this->account_details ) ) { + return; + } - if ( ! empty( $bacs_accounts ) ) { - foreach ( $bacs_accounts as $bacs_account ) { - $bacs_account = (object) $bacs_account; + // Get order and store in $order + $order = wc_get_order( $order_id ); - if ( $bacs_account->account_name || $bacs_account->bank_name ) { - echo '

    ' . implode( ' - ', array_filter( array( $bacs_account->account_name, $bacs_account->bank_name ) ) ) . '

    ' . PHP_EOL; + // Get the order country and country $locale + $country = $order->get_billing_country(); + $locale = $this->get_country_locale(); + + // Get sortcode label in the $locale array and use appropriate one + $sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' ); + + $bacs_accounts = apply_filters( 'woocommerce_bacs_accounts', $this->account_details ); + + if ( ! empty( $bacs_accounts ) ) { + $account_html = ''; + $has_details = false; + + foreach ( $bacs_accounts as $bacs_account ) { + $bacs_account = (object) $bacs_account; + + if ( $bacs_account->account_name ) { + $account_html .= '' . PHP_EOL; } - echo '
      ' . PHP_EOL; + $account_html .= '
        ' . PHP_EOL; - // BACS account fields shown on the thanks page and in emails + // BACS account fields shown on the thanks page and in emails $account_fields = apply_filters( 'woocommerce_bacs_account_fields', array( - 'account_number'=> array( - 'label' => __( 'Account Number', 'woocommerce' ), - 'value' => $bacs_account->account_number + 'bank_name' => array( + 'label' => __( 'Bank', 'woocommerce' ), + 'value' => $bacs_account->bank_name, ), - 'sort_code' => array( - 'label' => __( 'Sort Code', 'woocommerce' ), - 'value' => $bacs_account->sort_code + 'account_number' => array( + 'label' => __( 'Account number', 'woocommerce' ), + 'value' => $bacs_account->account_number, ), - 'iban' => array( + 'sort_code' => array( + 'label' => $sortcode, + 'value' => $bacs_account->sort_code, + ), + 'iban' => array( 'label' => __( 'IBAN', 'woocommerce' ), - 'value' => $bacs_account->iban + 'value' => $bacs_account->iban, ), - 'bic' => array( + 'bic' => array( 'label' => __( 'BIC', 'woocommerce' ), - 'value' => $bacs_account->bic - ) + 'value' => $bacs_account->bic, + ), ), $order_id ); - foreach ( $account_fields as $field_key => $field ) { - if ( ! empty( $field['value'] ) ) { - echo '
      • ' . esc_attr( $field['label'] ) . ': ' . wptexturize( $field['value'] ) . '
      • ' . PHP_EOL; - } + foreach ( $account_fields as $field_key => $field ) { + if ( ! empty( $field['value'] ) ) { + $account_html .= '
      • ' . wp_kses_post( $field['label'] ) . ': ' . wp_kses_post( wptexturize( $field['value'] ) ) . '
      • ' . PHP_EOL; + $has_details = true; + } } - echo '
      '; - } - } - } + $account_html .= '
    '; + } - /** - * Process the payment and return the result - * - * @param int $order_id - * @return array - */ - public function process_payment( $order_id ) { + if ( $has_details ) { + echo '

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

    ' . PHP_EOL . $account_html . '
    '; + } + } - $order = get_order( $order_id ); + } + + /** + * Process the payment and return the result. + * + * @param int $order_id + * @return array + */ + public function process_payment( $order_id ) { + + $order = wc_get_order( $order_id ); // Mark as on-hold (we're awaiting the payment) $order->update_status( 'on-hold', __( 'Awaiting BACS payment', 'woocommerce' ) ); // Reduce stock levels - $order->reduce_order_stock(); + wc_reduce_stock_levels( $order_id ); // Remove cart WC()->cart->empty_cart(); // Return thankyou redirect return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $order ) + 'result' => 'success', + 'redirect' => $this->get_return_url( $order ), ); - } + + } + + /** + * Get country locale if localized. + * + * @return array + */ + public function get_country_locale() { + + if ( empty( $this->locale ) ) { + + // Locale information to be used - only those that are not 'Sort Code' + $this->locale = apply_filters( 'woocommerce_get_bacs_locale', array( + 'AU' => array( + 'sortcode' => array( + 'label' => __( 'BSB', 'woocommerce' ), + ), + ), + 'CA' => array( + 'sortcode' => array( + 'label' => __( 'Bank transit number', 'woocommerce' ), + ), + ), + 'IN' => array( + 'sortcode' => array( + 'label' => __( 'IFSC', 'woocommerce' ), + ), + ), + 'IT' => array( + 'sortcode' => array( + 'label' => __( 'Branch sort', 'woocommerce' ), + ), + ), + 'NZ' => array( + 'sortcode' => array( + 'label' => __( 'Bank code', 'woocommerce' ), + ), + ), + 'SE' => array( + 'sortcode' => array( + 'label' => __( 'Bank code', 'woocommerce' ), + ), + ), + 'US' => array( + 'sortcode' => array( + 'label' => __( 'Routing number', 'woocommerce' ), + ), + ), + 'ZA' => array( + 'sortcode' => array( + 'label' => __( 'Branch code', 'woocommerce' ), + ), + ), + ) ); + + } + + return $this->locale; + + } } diff --git a/includes/gateways/cheque/class-wc-gateway-cheque.php b/includes/gateways/cheque/class-wc-gateway-cheque.php old mode 100755 new mode 100644 index 09069506e6f..53fde62bc16 --- a/includes/gateways/cheque/class-wc-gateway-cheque.php +++ b/includes/gateways/cheque/class-wc-gateway-cheque.php @@ -1,9 +1,11 @@ id = 'cheque'; - $this->icon = apply_filters('woocommerce_cheque_icon', ''); + $this->icon = apply_filters( 'woocommerce_cheque_icon', '' ); $this->has_fields = false; - $this->method_title = __( 'Cheque', 'woocommerce' ); - $this->method_description = __( 'Allows cheque payments. Why would you take cheques in this day and age? Well you probably wouldn\'t but it does allow you to make test purchases for testing order emails and the \'success\' pages etc.', 'woocommerce' ); + $this->method_title = _x( 'Check payments', 'Check payment method', 'woocommerce' ); + $this->method_description = __( 'Allows check payments. Why would you take checks in this day and age? Well you probably would not, but it does allow you to make test purchases for testing order emails and the success pages.', 'woocommerce' ); // Load the settings. $this->init_form_fields(); @@ -32,40 +34,40 @@ class WC_Gateway_Cheque extends WC_Payment_Gateway { // Define user set variables $this->title = $this->get_option( 'title' ); $this->description = $this->get_option( 'description' ); - $this->instructions = $this->get_option( 'instructions', $this->description ); + $this->instructions = $this->get_option( 'instructions' ); // Actions add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); - add_action( 'woocommerce_thankyou_cheque', array( $this, 'thankyou_page' ) ); + add_action( 'woocommerce_thankyou_cheque', array( $this, 'thankyou_page' ) ); - // Customer Emails - add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); - } + // Customer Emails + add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); + } - /** - * Initialise Gateway Settings Form Fields - */ - public function init_form_fields() { + /** + * Initialise Gateway Settings Form Fields. + */ + public function init_form_fields() { - $this->form_fields = array( + $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'woocommerce' ), 'type' => 'checkbox', - 'label' => __( 'Enable Cheque Payment', 'woocommerce' ), - 'default' => 'yes' + 'label' => __( 'Enable check payments', 'woocommerce' ), + 'default' => 'yes', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), - 'default' => __( 'Cheque Payment', 'woocommerce' ), + 'default' => _x( 'Check payments', 'Check payment method', 'woocommerce' ), 'desc_tip' => true, ), 'description' => array( 'title' => __( 'Description', 'woocommerce' ), 'type' => 'textarea', 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), - 'default' => __( 'Please send your cheque to Store Name, Store Street, Store Town, Store State / County, Store Postcode.', 'woocommerce' ), + 'default' => __( 'Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.', 'woocommerce' ), 'desc_tip' => true, ), 'instructions' => array( @@ -76,45 +78,46 @@ class WC_Gateway_Cheque extends WC_Payment_Gateway { 'desc_tip' => true, ), ); - } - - /** - * Output for the order received page. - */ - public function thankyou_page() { - if ( $this->instructions ) - echo wpautop( wptexturize( $this->instructions ) ); } - /** - * Add content to the WC emails. - * - * @access public - * @param WC_Order $order - * @param bool $sent_to_admin - * @param bool $plain_text - */ + /** + * Output for the order received page. + */ + public function thankyou_page() { + if ( $this->instructions ) { + echo wpautop( wptexturize( $this->instructions ) ); + } + } + + /** + * Add content to the WC emails. + * + * @access public + * @param WC_Order $order + * @param bool $sent_to_admin + * @param bool $plain_text + */ public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { - if ( $this->instructions && ! $sent_to_admin && 'cheque' === $order->payment_method && $order->has_status( 'on-hold' ) ) { + if ( $this->instructions && ! $sent_to_admin && 'cheque' === $order->get_payment_method() && $order->has_status( 'on-hold' ) ) { echo wpautop( wptexturize( $this->instructions ) ) . PHP_EOL; } } - /** - * Process the payment and return the result - * - * @param int $order_id - * @return array - */ + /** + * Process the payment and return the result. + * + * @param int $order_id + * @return array + */ public function process_payment( $order_id ) { - $order = get_order( $order_id ); + $order = wc_get_order( $order_id ); // Mark as on-hold (we're awaiting the cheque) - $order->update_status( 'on-hold', __( 'Awaiting cheque payment', 'woocommerce' ) ); + $order->update_status( 'on-hold', _x( 'Awaiting check payment', 'Check payment method', 'woocommerce' ) ); // Reduce stock levels - $order->reduce_order_stock(); + wc_reduce_stock_levels( $order_id ); // Remove cart WC()->cart->empty_cart(); @@ -122,7 +125,7 @@ class WC_Gateway_Cheque extends WC_Payment_Gateway { // Return thankyou redirect return array( 'result' => 'success', - 'redirect' => $this->get_return_url( $order ) + 'redirect' => $this->get_return_url( $order ), ); } -} \ No newline at end of file +} diff --git a/includes/gateways/class-wc-payment-gateway-cc.php b/includes/gateways/class-wc-payment-gateway-cc.php new file mode 100644 index 00000000000..608bb34e955 --- /dev/null +++ b/includes/gateways/class-wc-payment-gateway-cc.php @@ -0,0 +1,92 @@ +supports( 'tokenization' ) && is_checkout() ) { + $this->tokenization_script(); + $this->saved_payment_methods(); + $this->form(); + $this->save_payment_method_checkbox(); + } else { + $this->form(); + } + } + + /** + * Output field name HTML + * + * Gateways which support tokenization do not require names - we don't want the data to post to the server. + * + * @since 2.6.0 + * @param string $name + * @return string + */ + public function field_name( $name ) { + return $this->supports( 'tokenization' ) ? '' : ' name="' . esc_attr( $this->id . '-' . $name ) . '" '; + } + + /** + * Outputs fields for entering credit card information. + * @since 2.6.0 + */ + public function form() { + wp_enqueue_script( 'wc-credit-card-form' ); + + $fields = array(); + + $cvc_field = '

    + + field_name( 'card-cvc' ) . ' style="width:100px" /> +

    '; + + $default_fields = array( + 'card-number-field' => '

    + + field_name( 'card-number' ) . ' /> +

    ', + 'card-expiry-field' => '

    + + field_name( 'card-expiry' ) . ' /> +

    ', + ); + + if ( ! $this->supports( 'credit_card_form_cvc_on_saved_method' ) ) { + $default_fields['card-cvc-field'] = $cvc_field; + } + + $fields = wp_parse_args( $fields, apply_filters( 'woocommerce_credit_card_form_fields', $default_fields, $this->id ) ); + ?> + +
    + id ); ?> + + id ); ?> +
    +
    + supports( 'credit_card_form_cvc_on_saved_method' ) ) { + echo '
    ' . $cvc_field . '
    '; + } + } +} diff --git a/includes/gateways/class-wc-payment-gateway-echeck.php b/includes/gateways/class-wc-payment-gateway-echeck.php new file mode 100644 index 00000000000..652fa497fb6 --- /dev/null +++ b/includes/gateways/class-wc-payment-gateway-echeck.php @@ -0,0 +1,63 @@ +supports( 'tokenization' ) && is_checkout() ) { + $this->tokenization_script(); + $this->saved_payment_methods(); + $this->form(); + $this->save_payment_method_checkbox(); + } else { + $this->form(); + } + } + + /** + * Outputs fields for entering eCheck information. + * @since 2.6.0 + */ + public function form() { + $fields = array(); + + $default_fields = array( + 'routing-number' => '

    + + +

    ', + 'account-number' => '

    + + +

    ', + ); + + $fields = wp_parse_args( $fields, apply_filters( 'woocommerce_echeck_form_fields', $default_fields, $this->id ) ); + ?> + +
    + id ); ?> + + id ); ?> +
    +
    id = 'cod'; - $this->icon = apply_filters( 'woocommerce_cod_icon', '' ); - $this->method_title = __( 'Cash on Delivery', 'woocommerce' ); - $this->method_description = __( 'Have your customers pay with cash (or by other means) upon delivery.', 'woocommerce' ); - $this->has_fields = false; + // Setup general properties + $this->setup_properties(); // Load the settings $this->init_form_fields(); @@ -32,41 +31,52 @@ class WC_Gateway_COD extends WC_Payment_Gateway { // Get settings $this->title = $this->get_option( 'title' ); $this->description = $this->get_option( 'description' ); - $this->instructions = $this->get_option( 'instructions', $this->description ); + $this->instructions = $this->get_option( 'instructions' ); $this->enable_for_methods = $this->get_option( 'enable_for_methods', array() ); $this->enable_for_virtual = $this->get_option( 'enable_for_virtual', 'yes' ) === 'yes' ? true : false; add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); - add_action( 'woocommerce_thankyou_cod', array( $this, 'thankyou_page' ) ); + add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'thankyou_page' ) ); + add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'change_payment_complete_order_status' ), 10, 3 ); - // Customer Emails - add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); + // Customer Emails + add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 ); } - /** - * Initialise Gateway Settings Form Fields - */ - public function init_form_fields() { - $shipping_methods = array(); + /** + * Setup general properties for the gateway. + */ + protected function setup_properties() { + $this->id = 'cod'; + $this->icon = apply_filters( 'woocommerce_cod_icon', '' ); + $this->method_title = __( 'Cash on delivery', 'woocommerce' ); + $this->method_description = __( 'Have your customers pay with cash (or by other means) upon delivery.', 'woocommerce' ); + $this->has_fields = false; + } - if ( is_admin() ) - foreach ( WC()->shipping->load_shipping_methods() as $method ) { - $shipping_methods[ $method->id ] = $method->get_title(); - } + /** + * Initialise Gateway Settings Form Fields. + */ + public function init_form_fields() { + $shipping_methods = array(); - $this->form_fields = array( + foreach ( WC()->shipping()->load_shipping_methods() as $method ) { + $shipping_methods[ $method->id ] = $method->get_method_title(); + } + + $this->form_fields = array( 'enabled' => array( - 'title' => __( 'Enable COD', 'woocommerce' ), - 'label' => __( 'Enable Cash on Delivery', 'woocommerce' ), + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'label' => __( 'Enable cash on delivery', 'woocommerce' ), 'type' => 'checkbox', 'description' => '', - 'default' => 'no' + 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'woocommerce' ), 'type' => 'text', 'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ), - 'default' => __( 'Cash on Delivery', 'woocommerce' ), + 'default' => __( 'Cash on delivery', 'woocommerce' ), 'desc_tip' => true, ), 'description' => array( @@ -86,67 +96,64 @@ class WC_Gateway_COD extends WC_Payment_Gateway { 'enable_for_methods' => array( 'title' => __( 'Enable for shipping methods', 'woocommerce' ), 'type' => 'multiselect', - 'class' => 'chosen_select', - 'css' => 'width: 450px;', + 'class' => 'wc-enhanced-select', + 'css' => 'width: 400px;', 'default' => '', 'description' => __( 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', 'woocommerce' ), 'options' => $shipping_methods, 'desc_tip' => true, 'custom_attributes' => array( - 'data-placeholder' => __( 'Select shipping methods', 'woocommerce' ) - ) + 'data-placeholder' => __( 'Select shipping methods', 'woocommerce' ), + ), ), 'enable_for_virtual' => array( - 'title' => __( 'Enable for virtual orders', 'woocommerce' ), - 'label' => __( 'Enable COD if the order is virtual', 'woocommerce' ), + 'title' => __( 'Accept for virtual orders', 'woocommerce' ), + 'label' => __( 'Accept COD if the order is virtual', 'woocommerce' ), 'type' => 'checkbox', - 'default' => 'yes' - ) - ); - } + 'default' => 'yes', + ), + ); + } /** - * Check If The Gateway Is Available For Use + * Check If The Gateway Is Available For Use. * * @return bool */ public function is_available() { - $order = null; + $order = null; + $needs_shipping = false; - if ( ! $this->enable_for_virtual ) { - if ( WC()->cart && ! WC()->cart->needs_shipping() ) { - return false; - } + // Test if shipping is needed first + if ( WC()->cart && WC()->cart->needs_shipping() ) { + $needs_shipping = true; + } elseif ( is_page( wc_get_page_id( 'checkout' ) ) && 0 < get_query_var( 'order-pay' ) ) { + $order_id = absint( get_query_var( 'order-pay' ) ); + $order = wc_get_order( $order_id ); - if ( is_page( wc_get_page_id( 'checkout' ) ) && 0 < get_query_var( 'order-pay' ) ) { - $order_id = absint( get_query_var( 'order-pay' ) ); - $order = get_order( $order_id ); - - // Test if order needs shipping. - $needs_shipping = false; - - if ( 0 < sizeof( $order->get_items() ) ) { - foreach ( $order->get_items() as $item ) { - $_product = $order->get_product_from_item( $item ); - - if ( $_product->needs_shipping() ) { - $needs_shipping = true; - break; - } + // Test if order needs shipping. + if ( 0 < sizeof( $order->get_items() ) ) { + foreach ( $order->get_items() as $item ) { + $_product = $item->get_product(); + if ( $_product && $_product->needs_shipping() ) { + $needs_shipping = true; + break; } } - - $needs_shipping = apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping ); - - if ( $needs_shipping ) { - return false; - } } } - if ( ! empty( $this->enable_for_methods ) ) { + $needs_shipping = apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping ); - // Only apply if all packages are being shipped via local pickup + // Virtual order, with virtual disabled + if ( ! $this->enable_for_virtual && ! $needs_shipping ) { + return false; + } + + // Check methods + if ( ! empty( $this->enable_for_methods ) && $needs_shipping ) { + + // Only apply if all packages are being shipped via chosen methods, or order is virtual $chosen_shipping_methods_session = WC()->session->get( 'chosen_shipping_methods' ); if ( isset( $chosen_shipping_methods_session ) ) { @@ -161,7 +168,6 @@ class WC_Gateway_COD extends WC_Payment_Gateway { if ( $order->shipping_method ) { $check_method = $order->shipping_method; } - } elseif ( empty( $chosen_shipping_methods ) || sizeof( $chosen_shipping_methods ) > 1 ) { $check_method = false; } elseif ( sizeof( $chosen_shipping_methods ) == 1 ) { @@ -172,10 +178,14 @@ class WC_Gateway_COD extends WC_Payment_Gateway { return false; } + if ( strstr( $check_method, ':' ) ) { + $check_method = current( explode( ':', $check_method ) ); + } + $found = false; foreach ( $this->enable_for_methods as $method_id ) { - if ( strpos( $check_method, $method_id ) === 0 ) { + if ( $check_method === $method_id ) { $found = true; break; } @@ -190,21 +200,20 @@ class WC_Gateway_COD extends WC_Payment_Gateway { } - /** - * Process the payment and return the result - * - * @param int $order_id - * @return array - */ + /** + * Process the payment and return the result. + * + * @param int $order_id + * @return array + */ public function process_payment( $order_id ) { + $order = wc_get_order( $order_id ); - $order = get_order( $order_id ); - - // Mark as processing (payment won't be taken until delivery) - $order->update_status( 'processing', __( 'Payment to be made upon delivery.', 'woocommerce' ) ); + // Mark as processing or on-hold (payment won't be taken until delivery) + $order->update_status( apply_filters( 'woocommerce_cod_process_payment_order_status', $order->has_downloadable_item() ? 'on-hold' : 'processing', $order ), __( 'Payment to be made upon delivery.', 'woocommerce' ) ); // Reduce stock levels - $order->reduce_order_stock(); + wc_reduce_stock_levels( $order_id ); // Remove cart WC()->cart->empty_cart(); @@ -212,29 +221,45 @@ class WC_Gateway_COD extends WC_Payment_Gateway { // Return thankyou redirect return array( 'result' => 'success', - 'redirect' => $this->get_return_url( $order ) + 'redirect' => $this->get_return_url( $order ), ); } - /** - * Output for the order received page. - */ + /** + * Output for the order received page. + */ public function thankyou_page() { if ( $this->instructions ) { - echo wpautop( wptexturize( $this->instructions ) ); + echo wpautop( wptexturize( $this->instructions ) ); } } - /** - * Add content to the WC emails. - * - * @access public - * @param WC_Order $order - * @param bool $sent_to_admin - * @param bool $plain_text - */ + /** + * Change payment complete order status to completed for COD orders. + * + * @since 3.1.0 + * @param string $status + * @param int $order_id + * @param WC_Order $order + * @return string + */ + public function change_payment_complete_order_status( $status, $order_id = 0, $order = false ) { + if ( $order && 'cod' === $order->get_payment_method() ) { + $status = 'completed'; + } + return $status; + } + + /** + * Add content to the WC emails. + * + * @access public + * @param WC_Order $order + * @param bool $sent_to_admin + * @param bool $plain_text + */ public function email_instructions( $order, $sent_to_admin, $plain_text = false ) { - if ( $this->instructions && ! $sent_to_admin && 'cod' === $order->payment_method ) { + if ( $this->instructions && ! $sent_to_admin && $this->id === $order->get_payment_method() ) { echo wpautop( wptexturize( $this->instructions ) ) . PHP_EOL; } } diff --git a/includes/gateways/mijireh/assets/css/mijireh.css b/includes/gateways/mijireh/assets/css/mijireh.css deleted file mode 100644 index 185541beb15..00000000000 --- a/includes/gateways/mijireh/assets/css/mijireh.css +++ /dev/null @@ -1,155 +0,0 @@ -#mijireh_notice { - background: #5bc0de url(../images/mijireh-logo.png) no-repeat 15px 18px; - border: 1px solid #339bb9; - padding: 15px 15px 15px 152px !important; - box-shadow: inset 1px 1px 0 rgba( 255, 255, 255, 0.5 ), inset -1px -1px 0 rgba( 255, 255, 255, 0.5 ); - -moz-box-shadow: inset 1px 1px 0 rgba( 255, 255, 255, 0.5 ), inset -1px -1px 0 rgba( 255, 255, 255, 0.5 ); - -webkit-box-shadow: inset 1px 1px 0 rgba( 255, 255, 255, 0.5 ), inset -1px -1px 0 rgba( 255, 255, 255, 0.5 ); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - color: #fff; - text-shadow: 0 1px 0 #4a94ac; -} -#mijireh_notice.alert-danger, #mijireh_notice.alert-error { - background-color: #e0534e; - border: 1px solid #e0534e; - text-shadow: 0 1px 0 #e0534e; -} -#mijireh_notice.success { - background-color: #62c462; - border: 1px solid #62c462; - text-shadow: 0 1px 0 #4fbd4f; -} -#slurp_meta_box .inside { - padding:0 !important; - margin:0 !important; -} -#slurp_meta_box .alert-message { - margin:0 !important; -} -#slurp_meta_box h2 { - color:#fff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - margin: 0; -} -#slurp_meta_box a { - color: #fff; -} -#slurp_meta_box a.button-primary { - margin-right: 10px; -} -#slurp_meta_box p { - line-height: 22px; - margin-bottom: 1em; - margin-top: 0; -} - -/* Progress Bar Styles */ - .progress { - overflow: hidden; - line-height:18px!important; - height: 18px!important; - margin-bottom: 18px!important; - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f5f5f5), color-stop(100%, #f9f9f9)); - background: -webkit-linear-gradient(#f5f5f5, #f9f9f9); - background: -moz-linear-gradient(#f5f5f5, #f9f9f9); - background: -o-linear-gradient(#f5f5f5, #f9f9f9); - background: -ms-linear-gradient(#f5f5f5, #f9f9f9); - background: linear-gradient(#f5f5f5, #f9f9f9); - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - } - .progress .bar { - width: 0%; - height: 18px!important; - line-height: 18px!important; - color: white; - font-size: 12px; - text-align: center; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #149bdf), color-stop(100%, #0480be)); - background: -webkit-linear-gradient(#149bdf, #0480be); - background: -moz-linear-gradient(#149bdf, #0480be); - background: -o-linear-gradient(#149bdf, #0480be); - background: -ms-linear-gradient(#149bdf, #0480be); - background: linear-gradient(#149bdf, #0480be); - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(0, 0, 0, 0.4); - -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(0, 0, 0, 0.4); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(0, 0, 0, 0.4); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-transition: width 0.6s ease; - -moz-transition: width 0.6s ease; - transition: width 0.6s ease; - } - .progress-striped .bar { - background-color: #62c462; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - -moz-background-size: 40px 40px; - -o-background-size: 40px 40px; - background-size: 40px 40px; - } - .progress.active .bar { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; - } - .progress-danger .bar { - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35)); - background: -webkit-linear-gradient(#ee5f5b, #c43c35); - background: -moz-linear-gradient(#ee5f5b, #c43c35); - background: -o-linear-gradient(#ee5f5b, #c43c35); - background: -ms-linear-gradient(#ee5f5b, #c43c35); - background: linear-gradient(#ee5f5b, #c43c35); - } - .progress-danger.progress-striped .bar { - background-color: #ee5f5b; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - } - .progress-success .bar { - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #62c462), color-stop(100%, #57a957)); - background: -webkit-linear-gradient(#62c462, #57a957); - background: -moz-linear-gradient(#62c462, #57a957); - background: -o-linear-gradient(#62c462, #57a957); - background: -ms-linear-gradient(#62c462, #57a957); - background: linear-gradient(#62c462, #57a957); - } - .progress-success.progress-striped .bar { - background-color: #62c462; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - } - .progress.info .bar { - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5bc0de), color-stop(100%, #339bb9)); - background: -webkit-linear-gradient(#5bc0de, #339bb9); - background: -moz-linear-gradient(#5bc0de, #339bb9); - background: -o-linear-gradient(#5bc0de, #339bb9); - background: -ms-linear-gradient(#5bc0de, #339bb9); - background: linear-gradient(#5bc0de, #339bb9); - } - .progress-info.progress-striped .bar { - background-color: #5bc0de; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - } \ No newline at end of file diff --git a/includes/gateways/mijireh/assets/images/credit_cards.png b/includes/gateways/mijireh/assets/images/credit_cards.png deleted file mode 100644 index 13ac2c1cac5..00000000000 Binary files a/includes/gateways/mijireh/assets/images/credit_cards.png and /dev/null differ diff --git a/includes/gateways/mijireh/assets/images/mijireh-checkout-logo.png b/includes/gateways/mijireh/assets/images/mijireh-checkout-logo.png deleted file mode 100644 index 8b7d6b4bf5f..00000000000 Binary files a/includes/gateways/mijireh/assets/images/mijireh-checkout-logo.png and /dev/null differ diff --git a/includes/gateways/mijireh/assets/images/mijireh-logo.png b/includes/gateways/mijireh/assets/images/mijireh-logo.png deleted file mode 100644 index 5a2cb8f06f7..00000000000 Binary files a/includes/gateways/mijireh/assets/images/mijireh-logo.png and /dev/null differ diff --git a/includes/gateways/mijireh/assets/images/mijireh.png b/includes/gateways/mijireh/assets/images/mijireh.png deleted file mode 100644 index 6e1ad793250..00000000000 Binary files a/includes/gateways/mijireh/assets/images/mijireh.png and /dev/null differ diff --git a/includes/gateways/mijireh/assets/js/page_slurp.js b/includes/gateways/mijireh/assets/js/page_slurp.js deleted file mode 100644 index 959b4bb868c..00000000000 --- a/includes/gateways/mijireh/assets/js/page_slurp.js +++ /dev/null @@ -1,51 +0,0 @@ -(function($) { - - $('#page_slurp').click(function() { - var page_id = $(this).attr('rel'); - var data = { - action: 'page_slurp', - page_id: page_id - }; - - $('#page_slurp').attr('disabled', 'disabled'); - $('#slurp_progress').show(); - $('#slurp_progress_bar').html('Starting up'); - - $.post(ajaxurl, data, function(job_id) { - // The job id is the id for the page slurp job or 0 if the slurp failed - if(job_id.substring(0, 4) === 'http') { - var msg = 'Your PHP configuration does not allow for Page Slurp from within WordPress. Please log into your Mijireh account and Slurp the page by pasting in the URL to this page as shown below.\ - \n\n' + job_id + '\n\nPlease set this page to be publicly accessible during the Slurp then set it back to private after the Slurp is complete.'; - alert(msg); - } - else { - pusher = new Pusher('7dcd33b15307eb9be5fb'); - channel_name = 'slurp-' + job_id; - channel = pusher.subscribe(channel_name); - - channel.bind('status_changed', function (data) { - // console.log(data); - if(data.level == 'info') { - $('#slurp_progress_bar').html(data.message); - $('#slurp_progress_bar').width(data.progress + '%'); - } - - if(data.progress == 100) { - pusher.unsubscribe(channel_name); - $('#slurp_progress').hide(); - $('#page_slurp').removeAttr('disabled'); - } - }); - } - - }) - .error(function(response) { - $('#slurp_progress').hide(); - $('#page_slurp').removeAttr('disabled'); - alert('Please make sure your Mijireh access key is correct'); - }); - - return false; - }); - -})(jQuery.noConflict()); \ No newline at end of file diff --git a/includes/gateways/mijireh/class-wc-gateway-mijireh.php b/includes/gateways/mijireh/class-wc-gateway-mijireh.php deleted file mode 100644 index f242e8dd7cf..00000000000 --- a/includes/gateways/mijireh/class-wc-gateway-mijireh.php +++ /dev/null @@ -1,393 +0,0 @@ -id = 'mijireh_checkout'; - $this->method_title = __( 'Mijireh Checkout', 'woocommerce' ); - $this->icon = apply_filters( 'woocommerce_mijireh_checkout_icon', WC()->plugin_url() . '/includes/gateways/mijireh/assets/images/credit_cards.png' ); - $this->has_fields = false; - - // Load the settings. - $this->init_form_fields(); - $this->init_settings(); - - // Define user set variables - $this->access_key = $this->get_option( 'access_key' ); - $this->title = $this->get_option( 'title' ); - $this->description = $this->get_option( 'description' ); - - if ( $this->enabled && is_admin() ) { - $this->install_slurp_page(); - } - - // Save options - add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); - - // Payment listener/API hook - add_action( 'woocommerce_api_wc_gateway_mijireh', array( $this, 'mijireh_notification' ) ); - } - - /** - * install_slurp_page function. - * - * @access public - */ - public function install_slurp_page() { - $slurp_page_installed = get_option( 'slurp_page_installed', false ); - if ( $slurp_page_installed != 1 ) { - if( ! get_page_by_path( 'mijireh-secure-checkout' ) ) { - $page = array( - 'post_title' => 'Mijireh Secure Checkout', - 'post_name' => 'mijireh-secure-checkout', - 'post_parent' => 0, - 'post_status' => 'private', - 'post_type' => 'page', - 'comment_status' => 'closed', - 'ping_status' => 'closed', - 'post_content' => "

    Checkout

    \n\n{{mj-checkout-form}}", - ); - wp_insert_post( $page ); - } - update_option( 'slurp_page_installed', 1 ); - } - } - - /** - * mijireh_notification function. - * - * @access public - * @return void - */ - public function mijireh_notification() { - if ( isset( $_GET['order_number'] ) ) { - $this->init_mijireh(); - - try { - $mj_order = new Mijireh_Order( esc_attr( $_GET['order_number'] ) ); - $wc_order_id = $mj_order->get_meta_value( 'wc_order_id' ); - $wc_order = get_order( absint( $wc_order_id ) ); - - // Mark order complete - $wc_order->payment_complete(); - - // Empty cart and clear session - WC()->cart->empty_cart(); - - wp_redirect( $this->get_return_url( $wc_order ) ); - exit; - - } catch ( Mijireh_Exception $e ) { - wc_add_notice( __( 'Mijireh error:', 'woocommerce' ) . $e->getMessage(), 'error' ); - } - } - elseif ( isset( $_POST['page_id'] ) ) { - if ( isset( $_POST['access_key'] ) && $_POST['access_key'] == $this->access_key ) { - wp_update_post( array( 'ID' => $_POST['page_id'], 'post_status' => 'private' ) ); - } - } - } - - /** - * Initialise Gateway Settings Form Fields - * - * @access public - * @return void - */ - public function init_form_fields() { - $this->form_fields = array( - 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable Mijireh Checkout', 'woocommerce' ), - 'default' => 'no' - ), - 'access_key' => array( - 'title' => __( 'Access Key', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'The Mijireh access key for your store.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - ), - 'title' => array( - 'title' => __( 'Title', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), - 'default' => __( 'Credit Card', 'woocommerce' ), - 'desc_tip' => true, - ), - 'description' => array( - 'title' => __( 'Description', 'woocommerce' ), - 'type' => 'textarea', - 'default' => __( 'Pay securely with your credit card.', 'woocommerce' ), - 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ), - ), - ); - } - - /** - * Admin Panel Options - * - Options for bits like 'title' and availability on a country-by-country basis - * - * @access public - * @return void - */ - public function admin_options() { - ?> -

    - - access_key ) ) : ?> -
    -

    - Mijireh Checkout - -

    - -
    - -

    Mijireh Checkout

    - - - - generate_settings_html(); ?> -
    - init_mijireh(); - - $mj_order = new Mijireh_Order(); - $wc_order = get_order( $order_id ); - - // Avoid rounding issues altogether by sending the order as one lump - if ( get_option( 'woocommerce_prices_include_tax' ) == 'yes' ) { - - // Don't pass items - Pass 1 item for the order items overall - $item_names = array(); - - if ( sizeof( $wc_order->get_items() ) > 0 ) { - foreach ( $wc_order->get_items() as $item ) { - if ( $item['qty'] ) { - $item_names[] = $item['name'] . ' x ' . $item['qty']; - } - } - } - - $mj_order->add_item( sprintf( __( 'Order %s' , 'woocommerce'), $wc_order->get_order_number() ) . " - " . implode( ', ', $item_names ), number_format( $wc_order->get_total() - round( $wc_order->get_total_shipping() + $wc_order->get_shipping_tax(), 2 ) + $wc_order->get_order_discount(), 2, '.', '' ), 1 ); - - if ( ( $wc_order->get_total_shipping() + $wc_order->get_shipping_tax() ) > 0 ) { - $mj_order->shipping = number_format( $wc_order->get_total_shipping() + $wc_order->get_shipping_tax(), 2, '.', '' ); - } - $mj_order->show_tax = false; - - // No issues when prices exclude tax - } else { - // add items to order - $items = $wc_order->get_items(); - - foreach( $items as $item ) { - $product = $wc_order->get_product_from_item( $item ); - $mj_order->add_item( $item['name'], $wc_order->get_item_subtotal( $item, false, true ), $item['qty'], $product->get_sku() ); - } - - // Handle fees - $items = $wc_order->get_fees(); - - foreach( $items as $item ) { - $mj_order->add_item( $item['name'], number_format( $item['line_total'], 2, '.', ',' ), 1, '' ); - } - - $mj_order->shipping = round( $wc_order->get_total_shipping(), 2 ); - $mj_order->tax = $wc_order->get_total_tax(); - } - - // set order totals - $mj_order->total = $wc_order->get_total(); - $mj_order->discount = $wc_order->get_total_discount(); - - // add billing address to order - $billing = new Mijireh_Address(); - $billing->first_name = $wc_order->billing_first_name; - $billing->last_name = $wc_order->billing_last_name; - $billing->street = $wc_order->billing_address_1; - $billing->apt_suite = $wc_order->billing_address_2; - $billing->city = $wc_order->billing_city; - $billing->state_province = $wc_order->billing_state; - $billing->zip_code = $wc_order->billing_postcode; - $billing->country = $wc_order->billing_country; - $billing->company = $wc_order->billing_company; - $billing->phone = $wc_order->billing_phone; - - if ( $billing->validate() ) { - $mj_order->set_billing_address( $billing ); - } - - // add shipping address to order - $shipping = new Mijireh_Address(); - $shipping->first_name = $wc_order->shipping_first_name; - $shipping->last_name = $wc_order->shipping_last_name; - $shipping->street = $wc_order->shipping_address_1; - $shipping->apt_suite = $wc_order->shipping_address_2; - $shipping->city = $wc_order->shipping_city; - $shipping->state_province = $wc_order->shipping_state; - $shipping->zip_code = $wc_order->shipping_postcode; - $shipping->country = $wc_order->shipping_country; - $shipping->company = $wc_order->shipping_company; - - if ( $shipping->validate() ) { - $mj_order->set_shipping_address( $shipping ); - } - - // set order name - $mj_order->first_name = $wc_order->billing_first_name; - $mj_order->last_name = $wc_order->billing_last_name; - $mj_order->email = $wc_order->billing_email; - - // add meta data to identify woocommerce order - $mj_order->add_meta_data( 'wc_order_id', $order_id ); - - // Set URL for mijireh payment notificatoin - use WC API - $mj_order->return_url = WC()->api_request_url( 'WC_Gateway_Mijireh' ); - - // Identify woocommerce - $mj_order->partner_id = 'woo'; - - try { - $mj_order->create(); - $result = array( - 'result' => 'success', - 'redirect' => $mj_order->checkout_url - ); - return $result; - } catch ( Mijireh_Exception $e ) { - wc_add_notice( __( 'Mijireh error:', 'woocommerce' ) . $e->getMessage() . print_r( $mj_order, true ), 'error' ); - } - } - - /** - * init_mijireh function. - * - * @access public - */ - public function init_mijireh() { - if ( ! class_exists( 'Mijireh' ) ) { - require_once 'includes/Mijireh.php'; - - if ( ! isset( $this ) ) { - $settings = get_option( 'woocommerce_' . 'mijireh_checkout' . '_settings', null ); - $key = ! empty( $settings['access_key'] ) ? $settings['access_key'] : ''; - } else { - $key = $this->access_key; - } - - Mijireh::$access_key = $key; - } - } - - /** - * page_slurp function. - * - * @access public - */ - public static function page_slurp() { - self::init_mijireh(); - - $page = get_page( absint( $_POST['page_id'] ) ); - $url = get_permalink( $page->ID ); - $job_id = $url; - if ( wp_update_post( array( 'ID' => $page->ID, 'post_status' => 'publish' ) ) ) { - $job_id = Mijireh::slurp( $url, $page->ID, str_replace( 'https:', 'http:', add_query_arg( 'wc-api', 'WC_Gateway_Mijireh', home_url( '/' ) ) ) ); - } - echo $job_id; - die; - } - - /** - * add_page_slurp_meta function. - * - * @access public - */ - public static function add_page_slurp_meta() { - if ( self::is_slurp_page() ) { - wp_enqueue_style( 'mijireh_css', WC()->plugin_url() . '/includes/gateways/mijireh/assets/css/mijireh.css' ); - wp_enqueue_script( 'pusher', 'https://d3dy5gmtp8yhk7.cloudfront.net/1.11/pusher.min.js', null, false, true ); - wp_enqueue_script( 'page_slurp', WC()->plugin_url() . '/includes/gateways/mijireh/assets/js/page_slurp.js', array('jquery'), false, true ); - - add_meta_box( - 'slurp_meta_box', // $id - 'Mijireh Page Slurp', // $title - array( 'WC_Gateway_Mijireh', 'draw_page_slurp_meta_box' ), // $callback - 'page', // $page - 'normal', // $context - 'high' // $priority - ); - } - } - - /** - * is_slurp_page function. - * - * @access public - * @return bool - */ - public static function is_slurp_page() { - global $post; - $is_slurp = false; - if ( isset( $post ) && is_object( $post ) ) { - $content = $post->post_content; - if ( strpos( $content, '{{mj-checkout-form}}') !== false ) { - $is_slurp = true; - } - } - return $is_slurp; - } - - /** - * draw_page_slurp_meta_box function. - * - * @access public - * @param mixed $post - */ - public static function draw_page_slurp_meta_box( $post ) { - self::init_mijireh(); - - echo "
    "; - echo "

    Slurp your custom checkout page!

    "; - echo "

    Get the page designed just how you want and when you're ready, click the button below and slurp it right up.

    "; - echo ""; - echo "

    Slurp This Page! "; - echo 'Preview Checkout Page

    '; - echo "
    "; - } -} \ No newline at end of file diff --git a/includes/gateways/mijireh/includes/Address.php b/includes/gateways/mijireh/includes/Address.php deleted file mode 100755 index 3f4ed5c8129..00000000000 --- a/includes/gateways/mijireh/includes/Address.php +++ /dev/null @@ -1,52 +0,0 @@ -init(); - } - - public function init() { - $this->_data = array( - 'first_name' => '', - 'last_name' => '', - 'street' => '', - 'city' => '', - 'state_province' => '', - 'zip_code' => '', - 'country' => '', - 'company' => '', - 'apt_suite' => '', - 'phone' => '' - ); - } - - /** - * Check required fields - * @return bool - */ - public function validate() { - $is_valid = $this->_check_required_fields(); - return $is_valid; - } - - /** - * Return true if all of the required fields have a non-empty value - * - * @return boolean - */ - private function _check_required_fields() { - $pass = true; - $fields = array('street', 'city', 'zip_code', 'country'); - foreach($fields as $f) { - if(empty($this->_data[$f])) { - $pass = false; - $this->add_error("$f is required"); - } - } - return $pass; - } - -} diff --git a/includes/gateways/mijireh/includes/Item.php b/includes/gateways/mijireh/includes/Item.php deleted file mode 100755 index f2f107c246b..00000000000 --- a/includes/gateways/mijireh/includes/Item.php +++ /dev/null @@ -1,60 +0,0 @@ -_data = array( - 'name' => null, - 'price' => null, - 'quantity' => 1, - 'sku' => null - ); - } - - private function _check_required_fields() { - if(empty($this->_data['name'])) { - $this->add_error('item name is required.'); - } - - if(!is_numeric($this->_data['price'])) { - $this->add_error('price must be a number.'); - } - } - - private function _check_quantity() { - if($this->_data['quantity'] < 1) { - $this->add_error('quantity must be greater than or equal to 1'); - } - } - - public function __construct() { - $this->_init(); - } - - public function __get($key) { - $value = false; - if($key == 'total') { - $value = $this->_data['price'] * $this->_data['quantity']; - $value = number_format($value, 2, '.', ''); - } - else { - $value = parent::__get($key); - } - return $value; - } - - public function get_data() { - $data = parent::get_data(); - $data['total'] = $this->total; - return $data; - } - - public function validate() { - $this->_check_required_fields(); - $this->_check_quantity(); - return count($this->_errors) == 0; - } - -} \ No newline at end of file diff --git a/includes/gateways/mijireh/includes/Mijireh.php b/includes/gateways/mijireh/includes/Mijireh.php deleted file mode 100755 index dc60766f6be..00000000000 --- a/includes/gateways/mijireh/includes/Mijireh.php +++ /dev/null @@ -1,118 +0,0 @@ - $url, - 'page_id' => $page_id, - 'return_url' => $return_url - ); - $rest = new Mijireh_RestJSON(self::$url); - $rest->setupAuth(self::$access_key, ''); - $result = $rest->post('slurps', $data); - return $result['job_id']; - } - catch(Mijireh_Rest_Unauthorized $e) { - throw new Mijireh_Unauthorized("Unauthorized. Please check your api access key"); - } - catch(Mijireh_Rest_NotFound $e) { - throw new Mijireh_NotFound("Mijireh resource not found: " . $rest->last_request['url']); - } - catch(Mijireh_Rest_ClientError $e) { - throw new Mijireh_ClientError($e->getMessage()); - } - catch(Mijireh_Rest_ServerError $e) { - throw new Mijireh_ServerError($e->getMessage()); - } - catch(Mijireh_Rest_UnknownResponse $e) { - throw new Mijireh_Exception('Unable to slurp the URL: $url'); - } - } - - /** - * Return an array of store information - */ - public static function get_store_info() { - $rest = new Mijireh_RestJSON(self::$url); - $rest->setupAuth(self::$access_key, ''); - try { - $result = $rest->get('store'); - return $result; - } - catch(Mijireh_Rest_BadRequest $e) { - throw new Mijireh_BadRequest($e->getMessage()); - } - catch(Mijireh_Rest_Unauthorized $e) { - throw new Mijireh_Unauthorized("Unauthorized. Please check your api access key"); - } - catch(Mijireh_Rest_NotFound $e) { - throw new Mijireh_NotFound("Mijireh resource not found: " . $rest->last_request['url']); - } - catch(Mijireh_Rest_ClientError $e) { - throw new Mijireh_ClientError($e->getMessage()); - } - catch(Mijireh_Rest_ServerError $e) { - throw new Mijireh_ServerError($e->getMessage()); - } - } - - public static function preview_checkout_link() { - if(empty(Mijireh::$access_key)) { - throw new Mijireh_Exception('Access key required to view checkout preview'); - } - - return self::$base_url . 'checkout/' . self::$access_key; - } - -} diff --git a/includes/gateways/mijireh/includes/Model.php b/includes/gateways/mijireh/includes/Model.php deleted file mode 100755 index 1312c3260a0..00000000000 --- a/includes/gateways/mijireh/includes/Model.php +++ /dev/null @@ -1,135 +0,0 @@ -_data)) { - $this->_data[$key] = $value; - $success = true; - } - return $success; - } - - /** - * Get the value for the key from the private $_data array. - * - * Return false if the requested key does not exist - * - * @param string $key The key from the $_data array - * @return mixed - */ - public function __get($key) { - $value = false; - if(array_key_exists($key, $this->_data)) { - $value = $this->_data[$key]; - } - - /* - elseif(method_exists($this, $key)) { - $value = call_user_func_array(array($this, $key), func_get_args()); - } - */ - - return $value; - } - - /** - * Return true if the given $key in the private $_data array is set - * - * @param string $key - * @return boolean - */ - public function __isset($key) { - return isset($this->_data[$key]); - } - - /** - * Set the value of the $_data array to null for the given key. - * - * @param string $key - * @return void - */ - public function __unset($key) { - if(array_key_exists($key, $this->_data)) { - $this->_data[$key] = null; - } - } - - /** - * Return the private $_data array - * - * @return mixed - */ - public function get_data() { - return $this->_data; - } - - /** - * Return true if the given $key exists in the private $_data array - * - * @param string $key - * @return boolean - */ - public function field_exists($key) { - return array_key_exists($key, $this->_data); - } - - public function copy_from(array $data) { - foreach($data as $key => $value) { - if(array_key_exists($key, $this->_data)) { - $this->_data[$key] = $value; - } - } - } - - public function clear() { - foreach($this->_data as $key => $value) { - if($key == 'id') { - $this->_data[$key] = null; - } - else { - $this->_data[$key] = ''; - } - } - } - - public function add_error($error_message) { - if(!empty($error_message)) { - $this->_errors[] = $error_message; - } - } - - public function clear_errors() { - $this->_errors = array(); - } - - public function get_errors() { - return $this->_errors; - } - - public function get_error_lines($glue="\n") { - $error_lines = ''; - if(count($this->_errors)) { - $error_lines = implode($glue, $this->_errors); - } - return $error_lines; - } - - public function is_valid() { - return count($this->_errors) == 0; - } - -} diff --git a/includes/gateways/mijireh/includes/Order.php b/includes/gateways/mijireh/includes/Order.php deleted file mode 100755 index b2bab2dae16..00000000000 --- a/includes/gateways/mijireh/includes/Order.php +++ /dev/null @@ -1,348 +0,0 @@ -_data = array( - 'partner_id' => null, - 'order_number' => null, - 'mode' => null, - 'status' => null, - 'order_date' => null, - 'ip_address' => null, - 'checkout_url' => null, - 'total' => '', - 'return_url' => '', - 'items' => array(), - 'email' => '', - 'first_name' => '', - 'last_name' => '', - 'meta_data' => array(), - 'tax' => '', - 'shipping' => '', - 'discount' => '', - 'shipping_address' => array(), - 'billing_address' => array(), - 'show_tax' => true - ); - } - - public function __construct($order_number=null) { - $this->_init(); - if(isset($order_number)) { - $this->load($order_number); - } - } - - public function load($order_number) { - if(strlen(Mijireh::$access_key) < 5) { - throw new Mijireh_Exception('missing mijireh access key'); - } - - $rest = new Mijireh_RestJSON(Mijireh::$url); - $rest->setupAuth(Mijireh::$access_key, ''); - try { - $order_data = $rest->get("orders/$order_number"); - $this->copy_from($order_data); - return $this; - } - catch(Mijireh_Rest_BadRequest $e) { - throw new Mijireh_BadRequest($e->getMessage()); - } - catch(Mijireh_Rest_Unauthorized $e) { - throw new Mijireh_Unauthorized("Unauthorized. Please check your api access key"); - } - catch(Mijireh_Rest_NotFound $e) { - throw new Mijireh_NotFound("Mijireh resource not found: " . $rest->last_request['url']); - } - catch(Mijireh_Rest_ClientError $e) { - throw new Mijireh_ClientError($e->getMessage()); - } - catch(Mijireh_Rest_ServerError $e) { - throw new Mijireh_ServerError($e->getMessage()); - } - } - - public function copy_from($order_data) { - foreach($order_data as $key => $value) { - if($key == 'items') { - if(is_array($value)) { - $this->clear_items(); // Clear current items before adding new items. - foreach($value as $item_array) { - $item = new Mijireh_Item(); - $item->copy_from($item_array); - $this->add_item($item); - } - } - } - elseif($key == 'shipping_address') { - if(is_array($value)) { - $address = new Mijireh_Address(); - $address->copy_from($value); - $this->set_shipping_address($address); - } - } - elseif($key == 'billing_address') { - if(is_array($value)) { - $address = new Mijireh_Address(); - $address->copy_from($value); - $this->set_billing_address($address); - } - } - elseif($key == 'meta_data') { - if(is_array($value)) { - $this->clear_meta_data(); // Clear current meta data before adding new meta data - $this->_data['meta_data'] = $value; - } - } - else { - $this->$key = $value; - } - } - - if(!$this->validate()) { - throw new Mijireh_Exception('invalid order hydration: ' . $this->get_errors_lines()); - } - - return $this; - } - - public function create() { - if(strlen(Mijireh::$access_key) < 5) { - throw new Mijireh_Exception('missing mijireh access key'); - } - - if(!$this->validate()) { - $error_message = 'unable to create order: ' . $this->get_error_lines(); - throw new Mijireh_Exception($error_message); - } - - $rest = new Mijireh_RestJSON(Mijireh::$url); - $rest->setupAuth(Mijireh::$access_key, ''); - try { - $result = $rest->post('orders', $this->get_data()); - $this->copy_from($result); - return $this; - } - catch(Mijireh_Rest_BadRequest $e) { - throw new Mijireh_BadRequest($e->getMessage()); - } - catch(Mijireh_Rest_Unauthorized $e) { - throw new Mijireh_Unauthorized("Unauthorized. Please check your api access key"); - } - catch(Mijireh_Rest_NotFound $e) { - throw new Mijireh_NotFound("Mijireh resource not found: " . $rest->last_request['url']); - } - catch(Mijireh_Rest_ClientError $e) { - throw new Mijireh_ClientError($e->getMessage()); - } - catch(Mijireh_Rest_ServerError $e) { - throw new Mijireh_ServerError($e->getMessage()); - } - } - - /** - * If meta_data or shipping_address are empty, exclude them altogether. - */ - public function get_data() { - $data = parent::get_data(); - if(count($data['meta_data']) == 0) { unset($data['meta_data']); } - if(count($data['shipping_address']) == 0) { unset($data['shipping_address']); } - if(count($data['billing_address']) == 0) { unset($data['billing_address']); } - return $data; - } - - /** - * Add the specified item and price to the order. - * Return the total number of items in the order (including the one that was just added) - * - * @param Mijireh_Item|string $name - * @param int $price - * @param int $quantity - * @param string $sku - * @throws Mijireh_Exception - * @return int - */ - public function add_item($name, $price=0, $quantity=1, $sku='') { - $item = ''; - if(is_object($name) && get_class($name) == 'Mijireh_Item') { - $item = $name; - } - else { - $item = new Mijireh_Item(); - $item->name = $name; - $item->price = $price; - $item->quantity = $quantity; - $item->sku = $sku; - } - - if($item->validate()) { - $this->_data['items'][] = $item->get_data(); - return $this->item_count(); - } - else { - $errors = implode(' ', $item->get_errors()); - throw new Mijireh_Exception('unable to add invalid item to order :: ' . $errors); - } - } - - public function add_meta_data($key, $value) { - if(!is_array($this->_data['meta_data'])) { - $this->_data['meta_data'] = array(); - } - $this->_data['meta_data'][$key] = $value; - } - - /** - * Return the value associated with the given key in the order's meta data. - * - * If the key does not exist, return false. - */ - public function get_meta_value($key) { - $value = false; - if(isset($this->_data['meta_data'][$key])) { - $value = $this->_data['meta_data'][$key]; - } - return $value; - } - - public function item_count() { - $item_count = 0; - if(is_array($this->_data['items'])) { - $item_count = count($this->_data['items']); - } - return $item_count; - } - - public function get_items() { - $items = array(); - foreach($this->_data['items'] as $item_data) { - $item = new Mijireh_Item(); - $item->copy_from($item_data); - } - } - - public function clear_items() { - $this->_data['items'] = array(); - } - - public function clear_meta_data() { - $this->_data['meta_data'] = array(); - } - - public function validate() { - $this->_check_total(); - $this->_check_return_url(); - $this->_check_items(); - return count($this->_errors) == 0; - } - - /** - * Alias for set_shipping_address() - */ - public function set_address(Mijireh_Address $address){ - $this->set_shipping_address($address); - } - - public function set_shipping_address(Mijireh_Address $address) { - if($address->validate()) { - $this->_data['shipping_address'] = $address->get_data(); - } - else { - throw new Mijireh_Exception('invalid shipping address'); - } - } - - public function set_billing_address(Mijireh_Address $address) { - if($address->validate()) { - $this->_data['billing_address'] = $address->get_data(); - } - else { - throw new Mijireh_Exception('invalid shipping address'); - } - } - - /** - * Alias for get_shipping_address() - */ - public function get_address() { - return $this->get_shipping_address(); - } - - public function get_shipping_address() { - $address = false; - if(is_array($this->_data['shipping_address'])) { - $address = new Mijireh_Address(); - $address->copy_from($this->_data['shipping_address']); - } - return $address; - } - - public function get_billing_address() { - $address = false; - if(is_array($this->_data['billing_address'])) { - $address = new Mijireh_Address(); - $address->copy_from($this->_data['billing_address']); - } - return $address; - } - - /** - * The order total must be greater than zero. - * - * Return true if valid, otherwise false. - * - * @return boolean - */ - private function _check_total() { - $is_valid = true; - if($this->_data['total'] <= 0) { - $this->add_error('order total must be greater than zero'); - $is_valid = false; - } - return $is_valid; - } - - /** - * The return url must be provided and must start with http. - * - * Return true if valid, otherwise false - * - * @return boolean - */ - private function _check_return_url() { - $is_valid = false; - if(!empty($this->_data['return_url'])) { - $url = $this->_data['return_url']; - if('http' == strtolower(substr($url, 0, 4))) { - $is_valid = true; - } - else { - $this->add_error('return url is invalid'); - } - } - else { - $this->add_error('return url is required'); - } - return $is_valid; - } - - /** - * An order must contain at least one item - * - * Return true if the order has at least one item, otherwise false. - * - * @return boolean - */ - private function _check_items() { - $is_valid = true; - if(count($this->_data['items']) <= 0) { - $is_valid = false; - $this->add_error('the order must contain at least one item'); - } - return $is_valid; - } - -} diff --git a/includes/gateways/mijireh/includes/Rest.php b/includes/gateways/mijireh/includes/Rest.php deleted file mode 100755 index aef02d575ca..00000000000 --- a/includes/gateways/mijireh/includes/Rest.php +++ /dev/null @@ -1,246 +0,0 @@ - true, // return result instead of echoing - CURLOPT_SSL_VERIFYPEER => false, // stop cURL from verifying the peer's certificate - CURLOPT_MAXREDIRS => 10 // but don't redirect more than 10 times - ); - - public $base_url; - - public $last_response; - public $last_request; - - public $throw_exceptions = true; - - public function __construct($base_url, $curl_options=null) { - if (!function_exists('curl_init')) { - throw new Exception('CURL module not available! Mijireh_Rest requires CURL. See http://php.net/manual/en/book.curl.php'); - } - - if(isset($curl_options) && is_array($curl_options)) { - foreach($curl_options as $key => $value) { - - if($key == 'CURLOPT_FOLLOWLOCATION') { - // only enable CURLOPT_FOLLOWLOCATION if safe_mode and open_base_dir are not in use - if(ini_get('open_basedir') == '' && !ini_get('safe_mode')) { - $this->curl_opts['CURLOPT_FOLLOWLOCATION'] = true; - } - } - else { - $this->curl_opts[$key] = $value; - } - - } - } - - - $this->base_url = $base_url; - } - - // $auth can be 'basic' or 'digest' - public function setupAuth($user, $pass, $auth = 'basic') { - $this->curl_opts[CURLOPT_HTTPAUTH] = constant('CURLAUTH_'.strtoupper($auth)); - $this->curl_opts[CURLOPT_USERPWD] = $user . ":" . $pass; - } - - public function get($url) { - $curl = $this->prepRequest($this->curl_opts, $url); - $body = $this->doRequest($curl); - - $body = $this->processBody($body); - - return $body; - } - - public function post($url, $data, $headers=array()) { - $data = (is_array($data)) ? http_build_query($data) : $data; - - $curl_opts = $this->curl_opts; - $curl_opts[CURLOPT_CUSTOMREQUEST] = 'POST'; - $headers[] = 'Content-Length: '.strlen($data); - $curl_opts[CURLOPT_HTTPHEADER] = $headers; - $curl_opts[CURLOPT_POSTFIELDS] = $data; - - $curl = $this->prepRequest($curl_opts, $url); - $body = $this->doRequest($curl); - - $body = $this->processBody($body); - - return $body; - } - - public function put($url, $data, $headers=array()) { - $data = (is_array($data)) ? http_build_query($data) : $data; - - $curl_opts = $this->curl_opts; - $curl_opts[CURLOPT_CUSTOMREQUEST] = 'PUT'; - $headers[] = 'Content-Length: '.strlen($data); - $curl_opts[CURLOPT_HTTPHEADER] = $headers; - $curl_opts[CURLOPT_POSTFIELDS] = $data; - - $curl = $this->prepRequest($curl_opts, $url); - $body = $this->doRequest($curl); - - $body = $this->processBody($body); - - return $body; - } - - public function delete($url) { - $curl_opts = $this->curl_opts; - $curl_opts[CURLOPT_CUSTOMREQUEST] = 'DELETE'; - - $curl = $this->prepRequest($curl_opts, $url); - $body = $this->doRequest($curl); - - $body = $this->processBody($body); - - return $body; - } - - public function lastBody() { - return $this->last_response['body']; - } - - public function lastStatus() { - return $this->last_response['meta']['http_code']; - } - - protected function processBody($body) { - // Override this in classes that extend Mijireh_Rest. - // The body of every GET/POST/PUT/DELETE response goes through - // here prior to being returned. - return $body; - } - - protected function processError($body) { - // Override this in classes that extend Mijireh_Rest. - // The body of every erroneous (non-2xx/3xx) GET/POST/PUT/DELETE - // response goes through here prior to being used as the 'message' - // of the resulting Mijireh_Rest_Exception - return $body; - } - - - protected function prepRequest($opts, $url) { - if (strncmp($url, $this->base_url, strlen($this->base_url)) != 0) { - $url = $this->base_url . $url; - } - $curl = curl_init($url); - - foreach ($opts as $opt => $val) { - @curl_setopt($curl, $opt, $val); - } - - $this->last_request = array( - 'url' => $url - ); - - if (isset($opts[CURLOPT_CUSTOMREQUEST])) - $this->last_request['method'] = $opts[CURLOPT_CUSTOMREQUEST]; - else - $this->last_request['method'] = 'GET'; - - if (isset($opts[CURLOPT_POSTFIELDS])) - $this->last_request['data'] = $opts[CURLOPT_POSTFIELDS]; - - return $curl; - } - - private function doRequest($curl) { - - $body = curl_exec($curl); - $meta = curl_getinfo($curl); - - $this->last_response = array( - 'body' => $body, - 'meta' => $meta - ); - - curl_close($curl); - - $this->checkLastResponseForError(); - - return $body; - } - - protected function checkLastResponseForError() { - if ( !$this->throw_exceptions) - return; - - $meta = $this->last_response['meta']; - $body = $this->last_response['body']; - - if (!$meta) - return; - - $err = null; - switch ($meta['http_code']) { - case 400: - throw new Mijireh_Rest_BadRequest($this->processError($body)); - break; - case 401: - throw new Mijireh_Rest_Unauthorized($this->processError($body)); - break; - case 403: - throw new Mijireh_Rest_Forbidden($this->processError($body)); - break; - case 404: - throw new Mijireh_Rest_NotFound($this->processError($body)); - break; - case 405: - throw new Mijireh_Rest_MethodNotAllowed($this->processError($body)); - break; - case 409: - throw new Mijireh_Rest_Conflict($this->processError($body)); - break; - case 410: - throw new Mijireh_Rest_Gone($this->processError($body)); - break; - case 422: - // Unprocessable Entity -- see http://www.iana.org/assignments/http-status-codes - // This is now commonly used (in Rails, at least) to indicate - // a response to a request that is syntactically correct, - // but semantically invalid (for example, when trying to - // create a resource with some required fields missing) - throw new Mijireh_Rest_InvalidRecord($this->processError($body)); - break; - default: - if ($meta['http_code'] >= 400 && $meta['http_code'] <= 499) - throw new Mijireh_Rest_ClientError($this->processError($body)); - elseif ($meta['http_code'] >= 500 && $meta['http_code'] <= 599) - throw new Mijireh_Rest_ServerError($this->processError($body)); - elseif (!$meta['http_code'] || $meta['http_code'] >= 600) { - throw new Mijireh_Rest_UnknownResponse($this->processError($body)); - } - } - } -} - - -class Mijireh_Rest_Exception extends Exception { } -class Mijireh_Rest_UnknownResponse extends Mijireh_Rest_Exception { } - -/* 401-499 */ class Mijireh_Rest_ClientError extends Mijireh_Rest_Exception {} -/* 400 */ class Mijireh_Rest_BadRequest extends Mijireh_Rest_ClientError {} -/* 401 */ class Mijireh_Rest_Unauthorized extends Mijireh_Rest_ClientError {} -/* 403 */ class Mijireh_Rest_Forbidden extends Mijireh_Rest_ClientError {} -/* 404 */ class Mijireh_Rest_NotFound extends Mijireh_Rest_ClientError {} -/* 405 */ class Mijireh_Rest_MethodNotAllowed extends Mijireh_Rest_ClientError {} -/* 409 */ class Mijireh_Rest_Conflict extends Mijireh_Rest_ClientError {} -/* 410 */ class Mijireh_Rest_Gone extends Mijireh_Rest_ClientError {} -/* 422 */ class Mijireh_Rest_InvalidRecord extends Mijireh_Rest_ClientError {} - -/* 500-599 */ class Mijireh_Rest_ServerError extends Mijireh_Rest_Exception {} \ No newline at end of file diff --git a/includes/gateways/mijireh/includes/RestJSON.php b/includes/gateways/mijireh/includes/RestJSON.php deleted file mode 100755 index 79cab8ea5d0..00000000000 --- a/includes/gateways/mijireh/includes/RestJSON.php +++ /dev/null @@ -1,25 +0,0 @@ -id = 'paypal'; - $this->icon = apply_filters( 'woocommerce_paypal_icon', WC()->plugin_url() . '/assets/images/icons/paypal.png' ); - $this->has_fields = false; - $this->order_button_text = __( 'Proceed to PayPal', 'woocommerce' ); - $this->liveurl = 'https://www.paypal.com/cgi-bin/webscr'; - $this->testurl = 'https://www.sandbox.paypal.com/cgi-bin/webscr'; - $this->method_title = __( 'PayPal', 'woocommerce' ); - $this->view_transaction_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; - $this->notify_url = WC()->api_request_url( 'WC_Gateway_Paypal' ); + $this->id = 'paypal'; + $this->has_fields = false; + $this->order_button_text = __( 'Proceed to PayPal', 'woocommerce' ); + $this->method_title = __( 'PayPal', 'woocommerce' ); + $this->method_description = sprintf( __( 'PayPal Standard sends customers to PayPal to enter their payment information. PayPal IPN requires fsockopen/cURL support to update order statuses after payment. Check the system status page for more details.', 'woocommerce' ), admin_url( 'admin.php?page=wc-status' ) ); + $this->supports = array( + 'products', + 'refunds', + ); // Load the settings. $this->init_form_fields(); $this->init_settings(); - // Define user set variables - $this->title = $this->get_option( 'title' ); - $this->description = $this->get_option( 'description' ); - $this->email = $this->get_option( 'email' ); - $this->receiver_email = $this->get_option( 'receiver_email', $this->email ); - $this->testmode = $this->get_option( 'testmode' ); - $this->send_shipping = $this->get_option( 'send_shipping' ); - $this->address_override = $this->get_option( 'address_override' ); - $this->debug = $this->get_option( 'debug' ); - $this->form_submission_method = $this->get_option( 'form_submission_method' ) == 'yes' ? true : false; - $this->page_style = $this->get_option( 'page_style' ); - $this->invoice_prefix = $this->get_option( 'invoice_prefix', 'WC-' ); - $this->paymentaction = $this->get_option( 'paymentaction', 'sale' ); - $this->identity_token = $this->get_option( 'identity_token', '' ); + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->description = $this->get_option( 'description' ); + $this->testmode = 'yes' === $this->get_option( 'testmode', 'no' ); + $this->debug = 'yes' === $this->get_option( 'debug', 'no' ); + $this->email = $this->get_option( 'email' ); + $this->receiver_email = $this->get_option( 'receiver_email', $this->email ); + $this->identity_token = $this->get_option( 'identity_token' ); - // Logs - if ( 'yes' == $this->debug ) { - $this->log = new WC_Logger(); - } + self::$log_enabled = $this->debug; - // Actions - add_action( 'valid-paypal-standard-ipn-request', array( $this, 'successful_request' ) ); - add_action( 'woocommerce_receipt_paypal', array( $this, 'receipt_page' ) ); add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); - add_action( 'woocommerce_thankyou_paypal', array( $this, 'pdt_return_handler' ) ); - - // Payment listener/API hook - add_action( 'woocommerce_api_wc_gateway_paypal', array( $this, 'check_ipn_response' ) ); + add_action( 'woocommerce_order_status_on-hold_to_processing', array( $this, 'capture_payment' ) ); + add_action( 'woocommerce_order_status_on-hold_to_completed', array( $this, 'capture_payment' ) ); if ( ! $this->is_valid_for_use() ) { - $this->enabled = false; + $this->enabled = 'no'; + } else { + include_once( dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-ipn-handler.php' ); + new WC_Gateway_Paypal_IPN_Handler( $this->testmode, $this->receiver_email ); + + if ( $this->identity_token ) { + include_once( dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-pdt-handler.php' ); + new WC_Gateway_Paypal_PDT_Handler( $this->testmode, $this->identity_token ); + } } } /** - * Check if this gateway is enabled and available in the user's country + * Logging method. * - * @access public + * @param string $message Log message. + * @param string $level Optional. Default 'info'. + * emergency|alert|critical|error|warning|notice|info|debug + */ + public static function log( $message, $level = 'info' ) { + if ( self::$log_enabled ) { + if ( empty( self::$log ) ) { + self::$log = wc_get_logger(); + } + self::$log->log( $level, $message, array( 'source' => 'paypal' ) ); + } + } + + /** + * Get gateway icon. + * @return string + */ + public function get_icon() { + $icon_html = ''; + $icon = (array) $this->get_icon_image( WC()->countries->get_base_country() ); + + foreach ( $icon as $i ) { + $icon_html .= '' . esc_attr__( 'PayPal acceptance mark', 'woocommerce' ) . ''; + } + + $icon_html .= sprintf( '' . esc_attr__( 'What is PayPal?', 'woocommerce' ) . '', esc_url( $this->get_icon_url( WC()->countries->get_base_country() ) ) ); + + return apply_filters( 'woocommerce_gateway_icon', $icon_html, $this->id ); + } + + /** + * Get the link for an icon based on country. + * @param string $country + * @return string + */ + protected function get_icon_url( $country ) { + $url = 'https://www.paypal.com/' . strtolower( $country ); + $home_counties = array( 'BE', 'CZ', 'DK', 'HU', 'IT', 'JP', 'NL', 'NO', 'ES', 'SE', 'TR' ); + $countries = array( 'DZ', 'AU', 'BH', 'BQ', 'BW', 'CA', 'CN', 'CW', 'FI', 'FR', 'DE', 'GR', 'HK', 'IN', 'ID', 'JO', 'KE', 'KW', 'LU', 'MY', 'MA', 'OM', 'PH', 'PL', 'PT', 'QA', 'IE', 'RU', 'BL', 'SX', 'MF', 'SA', 'SG', 'SK', 'KR', 'SS', 'TW', 'TH', 'AE', 'GB', 'US', 'VN' ); + + if ( in_array( $country, $home_counties ) ) { + return $url . '/webapps/mpp/home'; + } elseif ( in_array( $country, $countries ) ) { + return $url . '/webapps/mpp/paypal-popup'; + } else { + return $url . '/cgi-bin/webscr?cmd=xpt/Marketing/general/WIPaypal-outside'; + } + } + + /** + * Get PayPal images for a country. + * @param string $country + * @return array of image URLs + */ + protected function get_icon_image( $country ) { + switch ( $country ) { + case 'US' : + case 'NZ' : + case 'CZ' : + case 'HU' : + case 'MY' : + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg'; + break; + case 'TR' : + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_odeme_secenekleri.jpg'; + break; + case 'GB' : + $icon = 'https://www.paypalobjects.com/webstatic/mktg/Logo/AM_mc_vs_ms_ae_UK.png'; + break; + case 'MX' : + $icon = array( + 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_visa_mastercard_amex.png', + 'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_debit_card_275x60.gif', + ); + break; + case 'FR' : + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_moyens_paiement_fr.jpg'; + break; + case 'AU' : + $icon = 'https://www.paypalobjects.com/webstatic/en_AU/mktg/logo/Solutions-graphics-1-184x80.jpg'; + break; + case 'DK' : + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_PayPal_betalingsmuligheder_dk.jpg'; + break; + case 'RU' : + $icon = 'https://www.paypalobjects.com/webstatic/ru_RU/mktg/business/pages/logo-center/AM_mc_vs_dc_ae.jpg'; + break; + case 'NO' : + $icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/banner_pl_just_pp_319x110.jpg'; + break; + case 'CA' : + $icon = 'https://www.paypalobjects.com/webstatic/en_CA/mktg/logo-image/AM_mc_vs_dc_ae.jpg'; + break; + case 'HK' : + $icon = 'https://www.paypalobjects.com/webstatic/en_HK/mktg/logo/AM_mc_vs_dc_ae.jpg'; + break; + case 'SG' : + $icon = 'https://www.paypalobjects.com/webstatic/en_SG/mktg/Logos/AM_mc_vs_dc_ae.jpg'; + break; + case 'TW' : + $icon = 'https://www.paypalobjects.com/webstatic/en_TW/mktg/logos/AM_mc_vs_dc_ae.jpg'; + break; + case 'TH' : + $icon = 'https://www.paypalobjects.com/webstatic/en_TH/mktg/Logos/AM_mc_vs_dc_ae.jpg'; + break; + case 'JP' : + $icon = 'https://www.paypal.com/ja_JP/JP/i/bnr/horizontal_solution_4_jcb.gif'; + break; + default : + $icon = WC_HTTPS::force_https_url( WC()->plugin_url() . '/includes/gateways/paypal/assets/images/paypal.png' ); + break; + } + return apply_filters( 'woocommerce_paypal_icon', $icon ); + } + + /** + * Check if this gateway is enabled and available in the user's country. * @return bool */ - function is_valid_for_use() { - if ( ! in_array( get_woocommerce_currency(), apply_filters( 'woocommerce_paypal_supported_currencies', array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB' ) ) ) ) { - return false; - } - - return true; + public function is_valid_for_use() { + return in_array( get_woocommerce_currency(), apply_filters( 'woocommerce_paypal_supported_currencies', array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB' ) ) ); } /** - * Admin Panel Options - * - Options for bits like 'title' and availability on a country-by-country basis + * Admin Panel Options. + * - Options for bits like 'title' and availability on a country-by-country basis. * * @since 1.0.0 */ public function admin_options() { - - ?> -

    -

    - - is_valid_for_use() ) : ?> - - - generate_settings_html(); + if ( $this->is_valid_for_use() ) { + parent::admin_options(); + } else { ?> -
    - - -

    :

    -

    :

    + form_fields = array( - 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable PayPal standard', 'woocommerce' ), - 'default' => 'yes' - ), - 'title' => array( - 'title' => __( 'Title', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), - 'default' => __( 'PayPal', 'woocommerce' ), - 'desc_tip' => true, - ), - 'description' => array( - 'title' => __( 'Description', 'woocommerce' ), - 'type' => 'textarea', - 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ), - 'default' => __( 'Pay via PayPal; you can pay with your credit card if you don\'t have a PayPal account', 'woocommerce' ) - ), - 'email' => array( - 'title' => __( 'PayPal Email', 'woocommerce' ), - 'type' => 'email', - 'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => 'you@youremail.com' - ), - 'receiver_email' => array( - 'title' => __( 'Receiver Email', 'woocommerce' ), - 'type' => 'email', - 'description' => __( 'If this differs from the email entered above, input your main receiver email for your PayPal account. This is used to validate IPN requests.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => 'you@youremail.com' - ), - 'identity_token' => array( - 'title' => __( 'PayPal Identity Token', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Website Payment Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ) - ), - 'invoice_prefix' => array( - 'title' => __( 'Invoice Prefix', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ), - 'default' => 'WC-', - 'desc_tip' => true, - ), - 'paymentaction' => array( - 'title' => __( 'Payment Action', 'woocommerce' ), - 'type' => 'select', - 'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ), - 'default' => 'sale', - 'desc_tip' => true, - 'options' => array( - 'sale' => __( 'Capture', 'woocommerce' ), - 'authorization' => __( 'Authorize', 'woocommerce' ) - ) - ), - 'form_submission_method' => array( - 'title' => __( 'Submission method', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Use form submission method.', 'woocommerce' ), - 'description' => __( 'Enable this to post order data to PayPal via a form instead of using a redirect/querystring.', 'woocommerce' ), - 'default' => 'no' - ), - 'page_style' => array( - 'title' => __( 'Page Style', 'woocommerce' ), - 'type' => 'text', - 'description' => __( 'Optionally enter the name of the page style you wish to use. These are defined within your PayPal account.', 'woocommerce' ), - 'default' => '', - 'desc_tip' => true, - 'placeholder' => __( 'Optional', 'woocommerce' ) - ), - 'shipping' => array( - 'title' => __( 'Shipping options', 'woocommerce' ), - 'type' => 'title', - 'description' => '', - ), - 'send_shipping' => array( - 'title' => __( 'Shipping details', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ), - 'description' => __( 'PayPal allows us to send 1 address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing.', 'woocommerce' ), - 'default' => 'no' - ), - 'address_override' => array( - 'title' => __( 'Address override', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ), - 'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ), - 'default' => 'no' - ), - 'testing' => array( - 'title' => __( 'Gateway Testing', 'woocommerce' ), - 'type' => 'title', - 'description' => '', - ), - 'testmode' => array( - 'title' => __( 'PayPal sandbox', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable PayPal sandbox', 'woocommerce' ), - 'default' => 'no', - 'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a developer account here.', 'woocommerce' ), 'https://developer.paypal.com/' ), - ), - 'debug' => array( - 'title' => __( 'Debug Log', 'woocommerce' ), - 'type' => 'checkbox', - 'label' => __( 'Enable logging', 'woocommerce' ), - 'default' => 'no', - 'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s', 'woocommerce' ), wc_get_log_file_path( 'paypal' ) ) - ) - ); + public function init_form_fields() { + $this->form_fields = include( 'includes/settings-paypal.php' ); } /** - * Limit the length of item names - * @param string $item_name + * Get the transaction URL. + * @param WC_Order $order * @return string */ - public function paypal_item_name( $item_name ) { - if ( strlen( $item_name ) > 127 ) { - $item_name = substr( $item_name, 0, 124 ) . '...'; + public function get_transaction_url( $order ) { + if ( $this->testmode ) { + $this->view_transaction_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; + } else { + $this->view_transaction_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; } - return html_entity_decode( $item_name, ENT_NOQUOTES, 'UTF-8' ); + return parent::get_transaction_url( $order ); } /** - * Get PayPal Args for passing to PP - * - * @access public - * @param mixed $order + * Process the payment and return the result. + * @param int $order_id * @return array */ - function get_paypal_args( $order ) { + public function process_payment( $order_id ) { + include_once( dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-request.php' ); - $order_id = $order->id; + $order = wc_get_order( $order_id ); + $paypal_request = new WC_Gateway_Paypal_Request( $this ); - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Generating payment form for order ' . $order->get_order_number() . '. Notify URL: ' . $this->notify_url ); - } - - if ( in_array( $order->billing_country, array( 'US','CA' ) ) ) { - $order->billing_phone = str_replace( array( '(', '-', ' ', ')', '.' ), '', $order->billing_phone ); - $phone_args = array( - 'night_phone_a' => substr( $order->billing_phone, 0, 3 ), - 'night_phone_b' => substr( $order->billing_phone, 3, 3 ), - 'night_phone_c' => substr( $order->billing_phone, 6, 4 ), - 'day_phone_a' => substr( $order->billing_phone, 0, 3 ), - 'day_phone_b' => substr( $order->billing_phone, 3, 3 ), - 'day_phone_c' => substr( $order->billing_phone, 6, 4 ) - ); - } else { - $phone_args = array( - 'night_phone_b' => $order->billing_phone, - 'day_phone_b' => $order->billing_phone - ); - } - - // PayPal Args - $paypal_args = array_merge( - array( - 'cmd' => '_cart', - 'business' => $this->email, - 'no_note' => 1, - 'currency_code' => get_woocommerce_currency(), - 'charset' => 'UTF-8', - 'rm' => is_ssl() ? 2 : 1, - 'upload' => 1, - 'return' => urlencode( esc_url( add_query_arg( 'utm_nooverride', '1', $this->get_return_url( $order ) ) ) ), - 'cancel_return' => urlencode( esc_url( $order->get_cancel_order_url() ) ), - 'page_style' => $this->page_style, - 'paymentaction' => $this->paymentaction, - 'bn' => 'WooThemes_Cart', - - // Order key + ID - 'invoice' => $this->invoice_prefix . ltrim( $order->get_order_number(), '#' ), - 'custom' => serialize( array( $order_id, $order->order_key ) ), - - // IPN - 'notify_url' => $this->notify_url, - - // Billing Address info - 'first_name' => $order->billing_first_name, - 'last_name' => $order->billing_last_name, - 'company' => $order->billing_company, - 'address1' => $order->billing_address_1, - 'address2' => $order->billing_address_2, - 'city' => $order->billing_city, - 'state' => $this->get_paypal_state( $order->billing_country, $order->billing_state ), - 'zip' => $order->billing_postcode, - 'country' => $order->billing_country, - 'email' => $order->billing_email - ), - $phone_args + return array( + 'result' => 'success', + 'redirect' => $paypal_request->get_request_url( $order, $this->testmode ), ); + } - // Shipping - if ( 'yes' == $this->send_shipping ) { - $paypal_args['address_override'] = ( $this->address_override == 'yes' ) ? 1 : 0; + /** + * Can the order be refunded via PayPal? + * @param WC_Order $order + * @return bool + */ + public function can_refund_order( $order ) { + return $order && $order->get_transaction_id(); + } - $paypal_args['no_shipping'] = 0; + /** + * Init the API class and set the username/password etc. + */ + protected function init_api() { + include_once( dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-api-handler.php' ); - // If we are sending shipping, send shipping address instead of billing - $paypal_args['first_name'] = $order->shipping_first_name; - $paypal_args['last_name'] = $order->shipping_last_name; - $paypal_args['company'] = $order->shipping_company; - $paypal_args['address1'] = $order->shipping_address_1; - $paypal_args['address2'] = $order->shipping_address_2; - $paypal_args['city'] = $order->shipping_city; - $paypal_args['state'] = $this->get_paypal_state( $order->shipping_country, $order->shipping_state ); - $paypal_args['country'] = $order->shipping_country; - $paypal_args['zip'] = $order->shipping_postcode; - } else { - $paypal_args['no_shipping'] = 1; + WC_Gateway_Paypal_API_Handler::$api_username = $this->get_option( 'api_username' ); + WC_Gateway_Paypal_API_Handler::$api_password = $this->get_option( 'api_password' ); + WC_Gateway_Paypal_API_Handler::$api_signature = $this->get_option( 'api_signature' ); + WC_Gateway_Paypal_API_Handler::$sandbox = $this->testmode; + } + + /** + * Process a refund if supported. + * @param int $order_id + * @param float $amount + * @param string $reason + * @return bool|WP_Error + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $order = wc_get_order( $order_id ); + + if ( ! $this->can_refund_order( $order ) ) { + $this->log( 'Refund Failed: No transaction ID', 'error' ); + return new WP_Error( 'error', __( 'Refund failed: No transaction ID', 'woocommerce' ) ); } - // If prices include tax or have order discounts, send the whole order as a single item - if ( get_option( 'woocommerce_prices_include_tax' ) == 'yes' || $order->get_order_discount() > 0 || ( sizeof( $order->get_items() ) + sizeof( $order->get_fees() ) ) >= 9 ) { + $this->init_api(); - // Discount - $paypal_args['discount_amount_cart'] = $order->get_order_discount(); + $result = WC_Gateway_Paypal_API_Handler::refund_transaction( $order, $amount, $reason ); - // Don't pass items - paypal borks tax due to prices including tax. PayPal has no option for tax inclusive pricing sadly. Pass 1 item for the order items overall - $item_names = array(); + if ( is_wp_error( $result ) ) { + $this->log( 'Refund Failed: ' . $result->get_error_message(), 'error' ); + return new WP_Error( 'error', $result->get_error_message() ); + } - if ( sizeof( $order->get_items() ) > 0 ) { - foreach ( $order->get_items() as $item ) { - if ( $item['qty'] ) { - $item_names[] = $item['name'] . ' x ' . $item['qty']; - } + $this->log( 'Refund Result: ' . wc_print_r( $result, true ) ); + + switch ( strtolower( $result->ACK ) ) { + case 'success': + case 'successwithwarning': + $order->add_order_note( sprintf( __( 'Refunded %1$s - Refund ID: %2$s', 'woocommerce' ), $result->GROSSREFUNDAMT, $result->REFUNDTRANSACTIONID ) ); + return true; + break; + } + + return isset( $result->L_LONGMESSAGE0 ) ? new WP_Error( 'error', $result->L_LONGMESSAGE0 ) : false; + } + + /** + * Capture payment when the order is changed from on-hold to complete or processing + * + * @param int $order_id + */ + public function capture_payment( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( 'paypal' === $order->get_payment_method() && 'pending' === get_post_meta( $order->get_id(), '_paypal_status', true ) && $order->get_transaction_id() ) { + $this->init_api(); + $result = WC_Gateway_Paypal_API_Handler::do_capture( $order ); + + if ( is_wp_error( $result ) ) { + $this->log( 'Capture Failed: ' . $result->get_error_message(), 'error' ); + $order->add_order_note( sprintf( __( 'Payment could not captured: %s', 'woocommerce' ), $result->get_error_message() ) ); + return; + } + + $this->log( 'Capture Result: ' . wc_print_r( $result, true ) ); + + if ( ! empty( $result->PAYMENTSTATUS ) ) { + switch ( $result->PAYMENTSTATUS ) { + case 'Completed' : + $order->add_order_note( sprintf( __( 'Payment of %1$s was captured - Auth ID: %2$s, Transaction ID: %3$s', 'woocommerce' ), $result->AMT, $result->AUTHORIZATIONID, $result->TRANSACTIONID ) ); + update_post_meta( $order->get_id(), '_paypal_status', $result->PAYMENTSTATUS ); + update_post_meta( $order->get_id(), '_transaction_id', $result->TRANSACTIONID ); + break; + default : + $order->add_order_note( sprintf( __( 'Payment could not captured - Auth ID: %1$s, Status: %2$s', 'woocommerce' ), $result->AUTHORIZATIONID, $result->PAYMENTSTATUS ) ); + break; } } - - $paypal_args['item_name_1'] = $this->paypal_item_name( sprintf( __( 'Order %s' , 'woocommerce'), $order->get_order_number() ) . " - " . implode( ', ', $item_names ) ); - $paypal_args['quantity_1'] = 1; - $paypal_args['amount_1'] = number_format( $order->get_total() - round( $order->get_total_shipping() + $order->get_shipping_tax(), 2 ) + $order->get_order_discount(), 2, '.', '' ); - - // Shipping Cost - // No longer using shipping_1 because - // a) paypal ignore it if *any* shipping rules are within paypal - // b) paypal ignore anything over 5 digits, so 999.99 is the max - if ( ( $order->get_total_shipping() + $order->get_shipping_tax() ) > 0 ) { - $paypal_args['item_name_2'] = $this->paypal_item_name( __( 'Shipping via', 'woocommerce' ) . ' ' . ucwords( $order->get_shipping_method() ) ); - $paypal_args['quantity_2'] = '1'; - $paypal_args['amount_2'] = number_format( $order->get_total_shipping() + $order->get_shipping_tax(), 2, '.', '' ); - } - - } else { - - // Tax - $paypal_args['tax_cart'] = $order->get_total_tax(); - - // Cart Contents - $item_loop = 0; - if ( sizeof( $order->get_items() ) > 0 ) { - foreach ( $order->get_items() as $item ) { - if ( $item['qty'] ) { - - $item_loop++; - - $product = $order->get_product_from_item( $item ); - - $item_name = $item['name']; - - $item_meta = new WC_Order_Item_Meta( $item['item_meta'] ); - if ( $meta = $item_meta->display( true, true ) ) { - $item_name .= ' ( ' . $meta . ' )'; - } - - $paypal_args[ 'item_name_' . $item_loop ] = $this->paypal_item_name( $item_name ); - $paypal_args[ 'quantity_' . $item_loop ] = $item['qty']; - $paypal_args[ 'amount_' . $item_loop ] = $order->get_item_subtotal( $item, false ); - - if ( $product->get_sku() ) { - $paypal_args[ 'item_number_' . $item_loop ] = $product->get_sku(); - } - } - } - } - - // Discount - if ( $order->get_cart_discount() > 0 ) { - $paypal_args['discount_amount_cart'] = round( $order->get_cart_discount(), 2 ); - } - - // Fees - if ( sizeof( $order->get_fees() ) > 0 ) { - foreach ( $order->get_fees() as $item ) { - $item_loop++; - - $paypal_args[ 'item_name_' . $item_loop ] = $this->paypal_item_name( $item['name'] ); - $paypal_args[ 'quantity_' . $item_loop ] = 1; - $paypal_args[ 'amount_' . $item_loop ] = $item['line_total']; - } - } - - // Shipping Cost item - paypal only allows shipping per item, we want to send shipping for the order - if ( $order->get_total_shipping() > 0 ) { - $item_loop++; - $paypal_args[ 'item_name_' . $item_loop ] = $this->paypal_item_name( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ) ); - $paypal_args[ 'quantity_' . $item_loop ] = '1'; - $paypal_args[ 'amount_' . $item_loop ] = number_format( $order->get_total_shipping(), 2, '.', '' ); - } - } - - $paypal_args = apply_filters( 'woocommerce_paypal_args', $paypal_args ); - - return $paypal_args; - } - - /** - * Generate the paypal button link - * - * @access public - * @param mixed $order_id - * @return string - */ - function generate_paypal_form( $order_id ) { - - $order = get_order( $order_id ); - - if ( 'yes' == $this->testmode ) { - $paypal_adr = $this->testurl . '?test_ipn=1&'; - } else { - $paypal_adr = $this->liveurl . '?'; - } - - $paypal_args = $this->get_paypal_args( $order ); - - $paypal_args_array = array(); - - foreach ( $paypal_args as $key => $value ) { - $paypal_args_array[] = ''; - } - - wc_enqueue_js( ' - $.blockUI({ - message: "' . esc_js( __( 'Thank you for your order. We are now redirecting you to PayPal to make payment.', 'woocommerce' ) ) . '", - baseZ: 99999, - overlayCSS: - { - background: "#fff", - opacity: 0.6 - }, - css: { - padding: "20px", - zindex: "9999999", - textAlign: "center", - color: "#555", - border: "3px solid #aaa", - backgroundColor:"#fff", - cursor: "wait", - lineHeight: "24px", - } - }); - jQuery("#submit_paypal_payment_form").click(); - ' ); - - return '
    - ' . implode( '', $paypal_args_array ) . ' - - - -
    '; - - } - - /** - * Process the payment and return the result - * - * @access public - * @param int $order_id - * @return array - */ - function process_payment( $order_id ) { - - $order = get_order( $order_id ); - - if ( ! $this->form_submission_method ) { - - $paypal_args = $this->get_paypal_args( $order ); - - $paypal_args = http_build_query( $paypal_args, '', '&' ); - - if ( 'yes' == $this->testmode ) { - $paypal_adr = $this->testurl . '?test_ipn=1&'; - } else { - $paypal_adr = $this->liveurl . '?'; - } - - return array( - 'result' => 'success', - 'redirect' => $paypal_adr . $paypal_args - ); - - } else { - - return array( - 'result' => 'success', - 'redirect' => $order->get_checkout_payment_url( true ) - ); - - } - - } - - /** - * Output for the order received page. - * - * @access public - * @return void - */ - function receipt_page( $order ) { - echo '

    ' . __( 'Thank you - your order is now pending payment. You should be automatically redirected to PayPal to make payment.', 'woocommerce' ) . '

    '; - - echo $this->generate_paypal_form( $order ); - } - - /** - * Check PayPal IPN validity - **/ - function check_ipn_request_is_valid( $ipn_response ) { - - // Get url - if ( 'yes' == $this->testmode ) { - $paypal_adr = $this->testurl; - } else { - $paypal_adr = $this->liveurl; - } - - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Checking IPN response is valid via ' . $paypal_adr . '...' ); - } - - // Get received values from post data - $validate_ipn = array( 'cmd' => '_notify-validate' ); - $validate_ipn += stripslashes_deep( $ipn_response ); - - // Send back post vars to paypal - $params = array( - 'body' => $validate_ipn, - 'sslverify' => false, - 'timeout' => 60, - 'httpversion' => '1.1', - 'compress' => false, - 'decompress' => false, - 'user-agent' => 'WooCommerce/' . WC()->version - ); - - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'IPN Request: ' . print_r( $params, true ) ); - } - - // Post back to get a response - $response = wp_remote_post( $paypal_adr, $params ); - - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'IPN Response: ' . print_r( $response, true ) ); - } - - // check to see if the request was valid - if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && ( strcmp( $response['body'], "VERIFIED" ) == 0 ) ) { - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Received valid response from PayPal' ); - } - - return true; - } - - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Received invalid response from PayPal' ); - if ( is_wp_error( $response ) ) { - $this->log->add( 'paypal', 'Error response: ' . $response->get_error_message() ); - } - } - - return false; - } - - /** - * Check for PayPal IPN Response - * - * @access public - * @return void - */ - function check_ipn_response() { - - @ob_clean(); - - $ipn_response = ! empty( $_POST ) ? $_POST : false; - - if ( $ipn_response && $this->check_ipn_request_is_valid( $ipn_response ) ) { - - header( 'HTTP/1.1 200 OK' ); - - do_action( "valid-paypal-standard-ipn-request", $ipn_response ); - - } else { - - wp_die( "PayPal IPN Request Failure", "PayPal IPN", array( 'response' => 200 ) ); - - } - - } - - /** - * Successful Payment! - * - * @access public - * @param array $posted - * @return void - */ - function successful_request( $posted ) { - - $posted = stripslashes_deep( $posted ); - - // Custom holds post ID - if ( ! empty( $posted['invoice'] ) && ! empty( $posted['custom'] ) ) { - - $order = $this->get_paypal_order( $posted['custom'], $posted['invoice'] ); - - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Found order #' . $order->id ); - } - - // Lowercase returned variables - $posted['payment_status'] = strtolower( $posted['payment_status'] ); - $posted['txn_type'] = strtolower( $posted['txn_type'] ); - - // Sandbox fix - if ( 1 == $posted['test_ipn'] && 'pending' == $posted['payment_status'] ) { - $posted['payment_status'] = 'completed'; - } - - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Payment status: ' . $posted['payment_status'] ); - } - - // We are here so lets check status and do actions - switch ( $posted['payment_status'] ) { - case 'completed' : - case 'pending' : - - // Check order not already completed - if ( $order->has_status( 'completed' ) ) { - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Aborting, Order #' . $order->id . ' is already complete.' ); - } - exit; - } - - // Check valid txn_type - $accepted_types = array( 'cart', 'instant', 'express_checkout', 'web_accept', 'masspay', 'send_money' ); - - if ( ! in_array( $posted['txn_type'], $accepted_types ) ) { - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Aborting, Invalid type:' . $posted['txn_type'] ); - } - exit; - } - - // Validate currency - if ( $order->get_order_currency() != $posted['mc_currency'] ) { - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Payment error: Currencies do not match (sent "' . $order->get_order_currency() . '" | returned "' . $posted['mc_currency'] . '")' ); - } - - // Put this order on-hold for manual checking - $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'woocommerce' ), $posted['mc_currency'] ) ); - exit; - } - - // Validate amount - if ( $order->get_total() != $posted['mc_gross'] ) { - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Payment error: Amounts do not match (gross ' . $posted['mc_gross'] . ')' ); - } - - // Put this order on-hold for manual checking - $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'woocommerce' ), $posted['mc_gross'] ) ); - exit; - } - - // Validate Email Address - if ( strcasecmp( trim( $posted['receiver_email'] ), trim( $this->receiver_email ) ) != 0 ) { - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', "IPN Response is for another one: {$posted['receiver_email']} our email is {$this->receiver_email}" ); - } - - // Put this order on-hold for manual checking - $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'woocommerce' ), $posted['receiver_email'] ) ); - - exit; - } - - // Store PP Details - if ( ! empty( $posted['payer_email'] ) ) { - update_post_meta( $order->id, 'Payer PayPal address', wc_clean( $posted['payer_email'] ) ); - } - if ( ! empty( $posted['first_name'] ) ) { - update_post_meta( $order->id, 'Payer first name', wc_clean( $posted['first_name'] ) ); - } - if ( ! empty( $posted['last_name'] ) ) { - update_post_meta( $order->id, 'Payer last name', wc_clean( $posted['last_name'] ) ); - } - if ( ! empty( $posted['payment_type'] ) ) { - update_post_meta( $order->id, 'Payment type', wc_clean( $posted['payment_type'] ) ); - } - - if ( $posted['payment_status'] == 'completed' ) { - $order->add_order_note( __( 'IPN payment completed', 'woocommerce' ) ); - $txn_id = ( ! empty( $posted['txn_id'] ) ) ? wc_clean( $posted['txn_id'] ) : ''; - $order->payment_complete( $txn_id ); - } else { - $order->update_status( 'on-hold', sprintf( __( 'Payment pending: %s', 'woocommerce' ), $posted['pending_reason'] ) ); - } - - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Payment complete.' ); - } - - break; - case 'denied' : - case 'expired' : - case 'failed' : - case 'voided' : - // Order failed - $order->update_status( 'failed', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) ); - break; - case 'refunded' : - - // Only handle full refunds, not partial - if ( $order->get_total() == ( $posted['mc_gross'] * -1 ) ) { - - // Mark order as refunded - $order->update_status( 'refunded', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) ); - - $this->send_ipn_email_notification( - sprintf( __( 'Payment for order %s refunded/reversed', 'woocommerce' ), $order->get_order_number() ), - sprintf( __( 'Order %s has been marked as refunded - PayPal reason code: %s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] ) - ); - } - - break; - case 'reversed' : - - // Mark order as refunded - $order->update_status( 'on-hold', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) ); - - $this->send_ipn_email_notification( - sprintf( __( 'Payment for order %s reversed', 'woocommerce' ), $order->get_order_number() ), - sprintf(__( 'Order %s has been marked on-hold due to a reversal - PayPal reason code: %s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] ) - ); - - break; - case 'canceled_reversal' : - $this->send_ipn_email_notification( - sprintf( __( 'Reversal cancelled for order %s', 'woocommerce' ), $order->get_order_number() ), - sprintf( __( 'Order %s has had a reversal cancelled. Please check the status of payment and update the order status accordingly.', 'woocommerce' ), $order->get_order_number() ) - ); - break; - default : - // No action - break; - } - - exit; - } - } - - /** - * Send a notification to the user handling orders. - * @param string $subject - * @param string $message - */ - public function send_ipn_email_notification( $subject, $message ) { - $new_order_settings = get_option( 'woocommerce_new_order_settings', array() ); - $mailer = WC()->mailer(); - $message = $mailer->wrap_message( $subject, $message ); - - $mailer->send( ! empty( $new_order_settings['recipient'] ) ? $new_order_settings['recipient'] : get_option( 'admin_email' ), $subject, $message ); - } - - /** - * Return handler - * - * Alternative to IPN - */ - public function pdt_return_handler() { - $posted = stripslashes_deep( $_REQUEST ); - - if ( ! empty( $this->identity_token ) && ! empty( $posted['cm'] ) ) { - - $order = $this->get_paypal_order( $posted['cm'] ); - - if ( ! $order->has_status( 'pending' ) ) { - return false; - } - - $posted['st'] = strtolower( $posted['st'] ); - - switch ( $posted['st'] ) { - case 'completed' : - - // Validate transaction - if ( 'yes' == $this->testmode ) { - $paypal_adr = $this->testurl; - } else { - $paypal_adr = $this->liveurl; - } - - $pdt = array( - 'body' => array( - 'cmd' => '_notify-synch', - 'tx' => $posted['tx'], - 'at' => $this->identity_token - ), - 'sslverify' => false, - 'timeout' => 60, - 'httpversion' => '1.1', - 'user-agent' => 'WooCommerce/' . WC_VERSION - ); - - // Post back to get a response - $response = wp_remote_post( $paypal_adr, $pdt ); - - if ( is_wp_error( $response ) ) { - return false; - } - - if ( ! strpos( $response['body'], "SUCCESS" ) === 0 ) { - return false; - } - - // Validate Amount - if ( $order->get_total() != $posted['amt'] ) { - - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Payment error: Amounts do not match (amt ' . $posted['amt'] . ')' ); - } - - // Put this order on-hold for manual checking - $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $posted['amt'] ) ); - return true; - - } else { - - // Store PP Details - $order->add_order_note( __( 'PDT payment completed', 'woocommerce' ) ); - $txn_id = ( ! empty( $posted['tx'] ) ) ? wc_clean( $posted['tx'] ) : ''; - $order->payment_complete( $txn_id ); - return true; - } - - break; - } - } - - return false; - } - - /** - * get_paypal_order function. - * - * @param string $custom - * @param string $invoice - * @return WC_Order object - */ - private function get_paypal_order( $custom, $invoice = '' ) { - $custom = maybe_unserialize( $custom ); - - // Backwards comp for IPN requests - if ( is_numeric( $custom ) ) { - $order_id = (int) $custom; - $order_key = $invoice; - } elseif( is_string( $custom ) ) { - $order_id = (int) str_replace( $this->invoice_prefix, '', $custom ); - $order_key = $custom; - } else { - list( $order_id, $order_key ) = $custom; - } - - $order = get_order( $order_id ); - - if ( ! isset( $order->id ) ) { - // We have an invalid $order_id, probably because invoice_prefix has changed - $order_id = wc_get_order_id_by_order_key( $order_key ); - $order = get_order( $order_id ); - } - - // Validate key - if ( $order->order_key !== $order_key ) { - if ( 'yes' == $this->debug ) { - $this->log->add( 'paypal', 'Error: Order Key does not match invoice.' ); - } - exit; - } - - return $order; - } - - /** - * Get the state to send to paypal - * @param string $cc - * @param string $state - * @return string - */ - public function get_paypal_state( $cc, $state ) { - if ( 'US' === $cc ) { - return $state; - } - - $states = WC()->countries->get_states( $cc ); - - if ( isset( $states[ $state ] ) ) { - return $states[ $state ]; - } - - return $state; } } diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php new file mode 100644 index 00000000000..9ed597cb91c --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php @@ -0,0 +1,156 @@ + '84.0', + 'SIGNATURE' => self::$api_signature, + 'USER' => self::$api_username, + 'PWD' => self::$api_password, + 'METHOD' => 'DoCapture', + 'AUTHORIZATIONID' => $order->get_transaction_id(), + 'AMT' => number_format( is_null( $amount ) ? $order->get_total() : $amount, 2, '.', '' ), + 'CURRENCYCODE' => $order->get_currency(), + 'COMPLETETYPE' => 'Complete', + ); + return apply_filters( 'woocommerce_paypal_capture_request', $request, $order, $amount ); + } + + /** + * Get refund request args. + * @param WC_Order $order + * @param float $amount + * @param string $reason + * @return array + */ + public static function get_refund_request( $order, $amount = null, $reason = '' ) { + $request = array( + 'VERSION' => '84.0', + 'SIGNATURE' => self::$api_signature, + 'USER' => self::$api_username, + 'PWD' => self::$api_password, + 'METHOD' => 'RefundTransaction', + 'TRANSACTIONID' => $order->get_transaction_id(), + 'NOTE' => html_entity_decode( wc_trim_string( $reason, 255 ), ENT_NOQUOTES, 'UTF-8' ), + 'REFUNDTYPE' => 'Full', + ); + if ( ! is_null( $amount ) ) { + $request['AMT'] = number_format( $amount, 2, '.', '' ); + $request['CURRENCYCODE'] = $order->get_currency(); + $request['REFUNDTYPE'] = 'Partial'; + } + return apply_filters( 'woocommerce_paypal_refund_request', $request, $order, $amount, $reason ); + } + + /** + * Capture an authorization. + * @param WC_Order $order + * @param float $amount + * @return object Either an object of name value pairs for a success, or a WP_ERROR object. + */ + public static function do_capture( $order, $amount = null ) { + $raw_response = wp_safe_remote_post( + self::$sandbox ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp', + array( + 'method' => 'POST', + 'body' => self::get_capture_request( $order, $amount ), + 'timeout' => 70, + 'user-agent' => 'WooCommerce/' . WC()->version, + 'httpversion' => '1.1', + ) + ); + + WC_Gateway_Paypal::log( 'DoCapture Response: ' . wc_print_r( $raw_response, true ) ); + + if ( empty( $raw_response['body'] ) ) { + return new WP_Error( 'paypal-api', 'Empty Response' ); + } elseif ( is_wp_error( $raw_response ) ) { + return $raw_response; + } + + parse_str( $raw_response['body'], $response ); + + return (object) $response; + } + + /** + * Refund an order via PayPal. + * @param WC_Order $order + * @param float $amount + * @param string $reason + * @return object Either an object of name value pairs for a success, or a WP_ERROR object. + */ + public static function refund_transaction( $order, $amount = null, $reason = '' ) { + $raw_response = wp_safe_remote_post( + self::$sandbox ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp', + array( + 'method' => 'POST', + 'body' => self::get_refund_request( $order, $amount, $reason ), + 'timeout' => 70, + 'user-agent' => 'WooCommerce/' . WC()->version, + 'httpversion' => '1.1', + ) + ); + + WC_Gateway_Paypal::log( 'Refund Response: ' . wc_print_r( $raw_response, true ) ); + + if ( empty( $raw_response['body'] ) ) { + return new WP_Error( 'paypal-api', 'Empty Response' ); + } elseif ( is_wp_error( $raw_response ) ) { + return $raw_response; + } + + parse_str( $raw_response['body'], $response ); + + return (object) $response; + } +} + +/** + * Here for backwards compatibility. + * @since 3.0.0 + */ +class WC_Gateway_Paypal_Refund extends WC_Gateway_Paypal_API_Handler { + public static function get_request( $order, $amount = null, $reason = '' ) { + return self::get_refund_request( $order, $amount, $reason ); + } + public static function refund_order( $order, $amount = null, $reason = '', $sandbox = false ) { + if ( $sandbox ) { + self::$sandbox = $sandbox; + } + $result = self::refund_transaction( $order, $amount, $reason ); + if ( is_wp_error( $result ) ) { + return $result; + } else { + return (array) $result; + } + } +} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php new file mode 100644 index 00000000000..2660e8af1a1 --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php @@ -0,0 +1,349 @@ +receiver_email = $receiver_email; + $this->sandbox = $sandbox; + } + + /** + * Check for PayPal IPN Response. + */ + public function check_response() { + if ( ! empty( $_POST ) && $this->validate_ipn() ) { + $posted = wp_unslash( $_POST ); + + // @codingStandardsIgnoreStart + do_action( 'valid-paypal-standard-ipn-request', $posted ); + // @codingStandardsIgnoreEnd + exit; + } + + wp_die( 'PayPal IPN Request Failure', 'PayPal IPN', array( 'response' => 500 ) ); + } + + /** + * There was a valid response. + * @param array $posted Post data after wp_unslash + */ + public function valid_response( $posted ) { + if ( ! empty( $posted['custom'] ) && ( $order = $this->get_paypal_order( $posted['custom'] ) ) ) { + + // Lowercase returned variables. + $posted['payment_status'] = strtolower( $posted['payment_status'] ); + + // Sandbox fix. + if ( ( empty( $posted['pending_reason'] ) || 'authorization' !== $posted['pending_reason'] ) && isset( $posted['test_ipn'] ) && 1 == $posted['test_ipn'] && 'pending' == $posted['payment_status'] ) { + $posted['payment_status'] = 'completed'; + } + + WC_Gateway_Paypal::log( 'Found order #' . $order->get_id() ); + WC_Gateway_Paypal::log( 'Payment status: ' . $posted['payment_status'] ); + + if ( method_exists( $this, 'payment_status_' . $posted['payment_status'] ) ) { + call_user_func( array( $this, 'payment_status_' . $posted['payment_status'] ), $order, $posted ); + } + } + } + + /** + * Check PayPal IPN validity. + */ + public function validate_ipn() { + WC_Gateway_Paypal::log( 'Checking IPN response is valid' ); + + // Get received values from post data + $validate_ipn = wp_unslash( $_POST ); + $validate_ipn['cmd'] = '_notify-validate'; + + // Send back post vars to paypal + $params = array( + 'body' => $validate_ipn, + 'timeout' => 60, + 'httpversion' => '1.1', + 'compress' => false, + 'decompress' => false, + 'user-agent' => 'WooCommerce/' . WC()->version, + ); + + // Post back to get a response. + $response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $params ); + + WC_Gateway_Paypal::log( 'IPN Request: ' . wc_print_r( $params, true ) ); + WC_Gateway_Paypal::log( 'IPN Response: ' . wc_print_r( $response, true ) ); + + // Check to see if the request was valid. + if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) { + WC_Gateway_Paypal::log( 'Received valid response from PayPal' ); + return true; + } + + WC_Gateway_Paypal::log( 'Received invalid response from PayPal' ); + + if ( is_wp_error( $response ) ) { + WC_Gateway_Paypal::log( 'Error response: ' . $response->get_error_message() ); + } + + return false; + } + + /** + * Check for a valid transaction type. + * @param string $txn_type + */ + protected function validate_transaction_type( $txn_type ) { + $accepted_types = array( 'cart', 'instant', 'express_checkout', 'web_accept', 'masspay', 'send_money', 'paypal_here' ); + + if ( ! in_array( strtolower( $txn_type ), $accepted_types ) ) { + WC_Gateway_Paypal::log( 'Aborting, Invalid type:' . $txn_type ); + exit; + } + } + + /** + * Check currency from IPN matches the order. + * @param WC_Order $order + * @param string $currency + */ + protected function validate_currency( $order, $currency ) { + if ( $order->get_currency() != $currency ) { + WC_Gateway_Paypal::log( 'Payment error: Currencies do not match (sent "' . $order->get_currency() . '" | returned "' . $currency . '")' ); + + // Put this order on-hold for manual checking. + $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'woocommerce' ), $currency ) ); + exit; + } + } + + /** + * Check payment amount from IPN matches the order. + * @param WC_Order $order + * @param int $amount + */ + protected function validate_amount( $order, $amount ) { + if ( number_format( $order->get_total(), 2, '.', '' ) != number_format( $amount, 2, '.', '' ) ) { + WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (gross ' . $amount . ')' ); + + // Put this order on-hold for manual checking. + $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'woocommerce' ), $amount ) ); + exit; + } + } + + /** + * Check receiver email from PayPal. If the receiver email in the IPN is different than what is stored in. + * WooCommerce -> Settings -> Checkout -> PayPal, it will log an error about it. + * @param WC_Order $order + * @param string $receiver_email + */ + protected function validate_receiver_email( $order, $receiver_email ) { + if ( strcasecmp( trim( $receiver_email ), trim( $this->receiver_email ) ) != 0 ) { + WC_Gateway_Paypal::log( "IPN Response is for another account: {$receiver_email}. Your email is {$this->receiver_email}" ); + + // Put this order on-hold for manual checking. + $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'woocommerce' ), $receiver_email ) ); + exit; + } + } + + /** + * Handle a completed payment. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_completed( $order, $posted ) { + if ( $order->has_status( wc_get_is_paid_statuses() ) ) { + WC_Gateway_Paypal::log( 'Aborting, Order #' . $order->get_id() . ' is already complete.' ); + exit; + } + + $this->validate_transaction_type( $posted['txn_type'] ); + $this->validate_currency( $order, $posted['mc_currency'] ); + $this->validate_amount( $order, $posted['mc_gross'] ); + $this->validate_receiver_email( $order, $posted['receiver_email'] ); + $this->save_paypal_meta_data( $order, $posted ); + + if ( 'completed' === $posted['payment_status'] ) { + if ( $order->has_status( 'cancelled' ) ) { + $this->payment_status_paid_cancelled_order( $order, $posted ); + } + + $this->payment_complete( $order, ( ! empty( $posted['txn_id'] ) ? wc_clean( $posted['txn_id'] ) : '' ), __( 'IPN payment completed', 'woocommerce' ) ); + + if ( ! empty( $posted['mc_fee'] ) ) { + // Log paypal transaction fee. + update_post_meta( $order->get_id(), 'PayPal Transaction Fee', wc_clean( $posted['mc_fee'] ) ); + } + } else { + if ( 'authorization' === $posted['pending_reason'] ) { + $this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) ); + } else { + $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $posted['pending_reason'] ) ); + } + } + } + + /** + * Handle a pending payment. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_pending( $order, $posted ) { + $this->payment_status_completed( $order, $posted ); + } + + /** + * Handle a failed payment. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_failed( $order, $posted ) { + $order->update_status( 'failed', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) ); + } + + /** + * Handle a denied payment. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_denied( $order, $posted ) { + $this->payment_status_failed( $order, $posted ); + } + + /** + * Handle an expired payment. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_expired( $order, $posted ) { + $this->payment_status_failed( $order, $posted ); + } + + /** + * Handle a voided payment. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_voided( $order, $posted ) { + $this->payment_status_failed( $order, $posted ); + } + + /** + * When a user cancelled order is marked paid. + * + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_paid_cancelled_order( $order, $posted ) { + $this->send_ipn_email_notification( + sprintf( __( 'Payment for cancelled order %s received', 'woocommerce' ), '' . $order->get_order_number() . '' ), + sprintf( __( 'Order #%1$s has been marked paid by PayPal IPN, but was previously cancelled. Admin handling required.', 'woocommerce' ), $order->get_order_number() ) + ); + } + + /** + * Handle a refunded order. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_refunded( $order, $posted ) { + // Only handle full refunds, not partial. + if ( $order->get_total() == ( $posted['mc_gross'] * -1 ) ) { + + // Mark order as refunded. + $order->update_status( 'refunded', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) ); + + $this->send_ipn_email_notification( + sprintf( __( 'Payment for order %s refunded', 'woocommerce' ), '' . $order->get_order_number() . '' ), + sprintf( __( 'Order #%1$s has been marked as refunded - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] ) + ); + } + } + + /** + * Handle a reversal. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_reversed( $order, $posted ) { + $order->update_status( 'on-hold', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) ); + + $this->send_ipn_email_notification( + sprintf( __( 'Payment for order %s reversed', 'woocommerce' ), '' . $order->get_order_number() . '' ), + sprintf( __( 'Order #%1$s has been marked on-hold due to a reversal - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), wc_clean( $posted['reason_code'] ) ) + ); + } + + /** + * Handle a cancelled reversal. + * @param WC_Order $order + * @param array $posted + */ + protected function payment_status_canceled_reversal( $order, $posted ) { + $this->send_ipn_email_notification( + sprintf( __( 'Reversal cancelled for order #%s', 'woocommerce' ), $order->get_order_number() ), + sprintf( __( 'Order #%1$s has had a reversal cancelled. Please check the status of payment and update the order status accordingly here: %2$s', 'woocommerce' ), $order->get_order_number(), esc_url( admin_url( 'post.php?post=' . $order->get_id() . '&action=edit' ) ) ) + ); + } + + /** + * Save important data from the IPN to the order. + * @param WC_Order $order + * @param array $posted + */ + protected function save_paypal_meta_data( $order, $posted ) { + if ( ! empty( $posted['payer_email'] ) ) { + update_post_meta( $order->get_id(), 'Payer PayPal address', wc_clean( $posted['payer_email'] ) ); + } + if ( ! empty( $posted['first_name'] ) ) { + update_post_meta( $order->get_id(), 'Payer first name', wc_clean( $posted['first_name'] ) ); + } + if ( ! empty( $posted['last_name'] ) ) { + update_post_meta( $order->get_id(), 'Payer last name', wc_clean( $posted['last_name'] ) ); + } + if ( ! empty( $posted['payment_type'] ) ) { + update_post_meta( $order->get_id(), 'Payment type', wc_clean( $posted['payment_type'] ) ); + } + if ( ! empty( $posted['txn_id'] ) ) { + update_post_meta( $order->get_id(), '_transaction_id', wc_clean( $posted['txn_id'] ) ); + } + if ( ! empty( $posted['payment_status'] ) ) { + update_post_meta( $order->get_id(), '_paypal_status', wc_clean( $posted['payment_status'] ) ); + } + } + + /** + * Send a notification to the user handling orders. + * @param string $subject + * @param string $message + */ + protected function send_ipn_email_notification( $subject, $message ) { + $new_order_settings = get_option( 'woocommerce_new_order_settings', array() ); + $mailer = WC()->mailer(); + $message = $mailer->wrap_message( $subject, $message ); + + $mailer->send( ! empty( $new_order_settings['recipient'] ) ? $new_order_settings['recipient'] : get_option( 'admin_email' ), strip_tags( $subject ), $message ); + } +} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php new file mode 100644 index 00000000000..de81d7ce827 --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php @@ -0,0 +1,130 @@ +identity_token = $identity_token; + $this->sandbox = $sandbox; + } + + /** + * Validate a PDT transaction to ensure its authentic. + * @param string $transaction + * @return bool|array False or result array + */ + protected function validate_transaction( $transaction ) { + $pdt = array( + 'body' => array( + 'cmd' => '_notify-synch', + 'tx' => $transaction, + 'at' => $this->identity_token, + ), + 'timeout' => 60, + 'httpversion' => '1.1', + 'user-agent' => 'WooCommerce/' . WC_VERSION, + ); + + // Post back to get a response. + $response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $pdt ); + + if ( is_wp_error( $response ) || ! strpos( $response['body'], "SUCCESS" ) === 0 ) { + return false; + } + + // Parse transaction result data + $transaction_result = array_map( 'wc_clean', array_map( 'urldecode', explode( "\n", $response['body'] ) ) ); + $transaction_results = array(); + + foreach ( $transaction_result as $line ) { + $line = explode( "=", $line ); + $transaction_results[ $line[0] ] = isset( $line[1] ) ? $line[1] : ''; + } + + if ( ! empty( $transaction_results['charset'] ) && function_exists( 'iconv' ) ) { + foreach ( $transaction_results as $key => $value ) { + $transaction_results[ $key ] = iconv( $transaction_results['charset'], 'utf-8', $value ); + } + } + + return $transaction_results; + } + + /** + * Check Response for PDT. + */ + public function check_response() { + if ( empty( $_REQUEST['cm'] ) || empty( $_REQUEST['tx'] ) || empty( $_REQUEST['st'] ) ) { + return; + } + + $order_id = wc_clean( stripslashes( $_REQUEST['cm'] ) ); + $status = wc_clean( strtolower( stripslashes( $_REQUEST['st'] ) ) ); + $amount = wc_clean( stripslashes( $_REQUEST['amt'] ) ); + $transaction = wc_clean( stripslashes( $_REQUEST['tx'] ) ); + + if ( ! ( $order = $this->get_paypal_order( $order_id ) ) || ! $order->has_status( 'pending' ) ) { + return false; + } + + $transaction_result = $this->validate_transaction( $transaction ); + + WC_Gateway_Paypal::log( 'PDT Transaction Result: ' . wc_print_r( $transaction_result, true ) ); + + update_post_meta( $order->get_id(), '_paypal_status', $status ); + update_post_meta( $order->get_id(), '_transaction_id', $transaction ); + + if ( $transaction_result ) { + if ( 'completed' === $status ) { + if ( $order->get_total() != $amount ) { + WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (amt ' . $amount . ')', 'error' ); + $this->payment_on_hold( $order, sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $amount ) ); + } else { + $this->payment_complete( $order, $transaction, __( 'PDT payment completed', 'woocommerce' ) ); + + // Log paypal transaction fee and other meta data. + if ( ! empty( $transaction_result['mc_fee'] ) ) { + update_post_meta( $order->get_id(), 'PayPal Transaction Fee', $transaction_result['mc_fee'] ); + } + if ( ! empty( $transaction_result['payer_email'] ) ) { + update_post_meta( $order->get_id(), 'Payer PayPal address', $transaction_result['payer_email'] ); + } + if ( ! empty( $transaction_result['first_name'] ) ) { + update_post_meta( $order->get_id(), 'Payer first name', $transaction_result['first_name'] ); + } + if ( ! empty( $transaction_result['last_name'] ) ) { + update_post_meta( $order->get_id(), 'Payer last name', $transaction_result['last_name'] ); + } + if ( ! empty( $transaction_result['payment_type'] ) ) { + update_post_meta( $order->get_id(), 'Payment type', $transaction_result['payment_type'] ); + } + } + } else { + if ( 'authorization' === $transaction_result['pending_reason'] ) { + $this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) ); + } else { + $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $transaction_result['pending_reason'] ) ); + } + } + } + } +} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php new file mode 100644 index 00000000000..44076dcd62b --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php @@ -0,0 +1,416 @@ +gateway = $gateway; + $this->notify_url = WC()->api_request_url( 'WC_Gateway_Paypal' ); + } + + /** + * Get the PayPal request URL for an order. + * @param WC_Order $order + * @param bool $sandbox + * @return string + */ + public function get_request_url( $order, $sandbox = false ) { + $paypal_args = http_build_query( $this->get_paypal_args( $order ), '', '&' ); + + WC_Gateway_Paypal::log( 'PayPal Request Args for order ' . $order->get_order_number() . ': ' . wc_print_r( $paypal_args, true ) ); + + if ( $sandbox ) { + return 'https://www.sandbox.paypal.com/cgi-bin/webscr?test_ipn=1&' . $paypal_args; + } else { + return 'https://www.paypal.com/cgi-bin/webscr?' . $paypal_args; + } + } + + /** + * Limit length of an arg. + * + * @param string $string + * @param integer $limit + * @return string + */ + protected function limit_length( $string, $limit = 127 ) { + if ( strlen( $string ) > $limit ) { + $string = substr( $string, 0, $limit - 3 ) . '...'; + } + return $string; + } + + /** + * Get PayPal Args for passing to PP. + * @param WC_Order $order + * @return array + */ + protected function get_paypal_args( $order ) { + WC_Gateway_Paypal::log( 'Generating payment form for order ' . $order->get_order_number() . '. Notify URL: ' . $this->notify_url ); + + return apply_filters( 'woocommerce_paypal_args', array_merge( + array( + 'cmd' => '_cart', + 'business' => $this->gateway->get_option( 'email' ), + 'no_note' => 1, + 'currency_code' => get_woocommerce_currency(), + 'charset' => 'utf-8', + 'rm' => is_ssl() ? 2 : 1, + 'upload' => 1, + 'return' => esc_url_raw( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ), + 'cancel_return' => esc_url_raw( $order->get_cancel_order_url_raw() ), + 'page_style' => $this->gateway->get_option( 'page_style' ), + 'image_url' => esc_url_raw( $this->gateway->get_option( 'image_url' ) ), + 'paymentaction' => $this->gateway->get_option( 'paymentaction' ), + 'bn' => 'WooThemes_Cart', + 'invoice' => $this->limit_length( $this->gateway->get_option( 'invoice_prefix' ) . $order->get_order_number(), 127 ), + 'custom' => json_encode( array( 'order_id' => $order->get_id(), 'order_key' => $order->get_order_key() ) ), + 'notify_url' => $this->limit_length( $this->notify_url, 255 ), + 'first_name' => $this->limit_length( $order->get_billing_first_name(), 32 ), + 'last_name' => $this->limit_length( $order->get_billing_last_name(), 64 ), + 'address1' => $this->limit_length( $order->get_billing_address_1(), 100 ), + 'address2' => $this->limit_length( $order->get_billing_address_2(), 100 ), + 'city' => $this->limit_length( $order->get_billing_city(), 40 ), + 'state' => $this->get_paypal_state( $order->get_billing_country(), $order->get_billing_state() ), + 'zip' => $this->limit_length( wc_format_postcode( $order->get_billing_postcode(), $order->get_billing_country() ), 32 ), + 'country' => $this->limit_length( $order->get_billing_country(), 2 ), + 'email' => $this->limit_length( $order->get_billing_email() ), + ), + $this->get_phone_number_args( $order ), + $this->get_shipping_args( $order ), + $this->get_line_item_args( $order ) + ), $order ); + } + + /** + * Get phone number args for paypal request. + * @param WC_Order $order + * @return array + */ + protected function get_phone_number_args( $order ) { + if ( in_array( $order->get_billing_country(), array( 'US', 'CA' ) ) ) { + $phone_number = str_replace( array( '(', '-', ' ', ')', '.' ), '', $order->get_billing_phone() ); + $phone_number = ltrim( $phone_number, '+1' ); + $phone_args = array( + 'night_phone_a' => substr( $phone_number, 0, 3 ), + 'night_phone_b' => substr( $phone_number, 3, 3 ), + 'night_phone_c' => substr( $phone_number, 6, 4 ), + ); + } else { + $phone_args = array( + 'night_phone_b' => $order->get_billing_phone(), + ); + } + return $phone_args; + } + + /** + * Get shipping args for paypal request. + * @param WC_Order $order + * @return array + */ + protected function get_shipping_args( $order ) { + $shipping_args = array(); + + if ( 'yes' == $this->gateway->get_option( 'send_shipping' ) ) { + $shipping_args['address_override'] = $this->gateway->get_option( 'address_override' ) === 'yes' ? 1 : 0; + $shipping_args['no_shipping'] = 0; + + // If we are sending shipping, send shipping address instead of billing + $shipping_args['first_name'] = $this->limit_length( $order->get_shipping_first_name(), 32 ); + $shipping_args['last_name'] = $this->limit_length( $order->get_shipping_last_name(), 64 ); + $shipping_args['address1'] = $this->limit_length( $order->get_shipping_address_1(), 100 ); + $shipping_args['address2'] = $this->limit_length( $order->get_shipping_address_2(), 100 ); + $shipping_args['city'] = $this->limit_length( $order->get_shipping_city(), 40 ); + $shipping_args['state'] = $this->get_paypal_state( $order->get_shipping_country(), $order->get_shipping_state() ); + $shipping_args['country'] = $this->limit_length( $order->get_shipping_country(), 2 ); + $shipping_args['zip'] = $this->limit_length( wc_format_postcode( $order->get_shipping_postcode(), $order->get_shipping_country() ), 32 ); + } else { + $shipping_args['no_shipping'] = 1; + } + + return $shipping_args; + } + + /** + * Get line item args for paypal request. + * @param WC_Order $order + * @return array + */ + protected function get_line_item_args( $order ) { + + /** + * Try passing a line item per product if supported. + */ + if ( ( ! wc_tax_enabled() || ! wc_prices_include_tax() ) && $this->prepare_line_items( $order ) ) { + + $line_item_args = array(); + $line_item_args['tax_cart'] = $this->number_format( $order->get_total_tax(), $order ); + + if ( $order->get_total_discount() > 0 ) { + $line_item_args['discount_amount_cart'] = $this->number_format( $this->round( $order->get_total_discount(), $order ), $order ); + } + + // Add shipping costs. Paypal ignores anything over 5 digits (999.99 is the max). + // We also check that shipping is not the **only** cost as PayPal won't allow payment + // if the items have no cost. + if ( $order->get_shipping_total() > 0 && $order->get_shipping_total() < 999.99 && $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) !== $this->number_format( $order->get_total(), $order ) ) { + $line_item_args['shipping_1'] = $this->number_format( $order->get_shipping_total(), $order ); + } elseif ( $order->get_shipping_total() > 0 ) { + $this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $order->get_shipping_total(), $order ) ); + } + + $line_item_args = array_merge( $line_item_args, $this->get_line_items() ); + + /** + * Send order as a single item. + * + * For shipping, we longer use shipping_1 because paypal ignores it if *any* shipping rules are within paypal, and paypal ignores anything over 5 digits (999.99 is the max). + */ + } else { + + $this->delete_line_items(); + + $line_item_args = array(); + $all_items_name = $this->get_order_item_names( $order ); + $this->add_line_item( $all_items_name ? $all_items_name : __( 'Order', 'woocommerce' ), 1, $this->number_format( $order->get_total() - $this->round( $order->get_shipping_total() + $order->get_shipping_tax(), $order ), $order ), $order->get_order_number() ); + + // Add shipping costs. Paypal ignores anything over 5 digits (999.99 is the max). + // We also check that shipping is not the **only** cost as PayPal won't allow payment + // if the items have no cost. + if ( $order->get_shipping_total() > 0 && $order->get_shipping_total() < 999.99 && $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) !== $this->number_format( $order->get_total(), $order ) ) { + $line_item_args['shipping_1'] = $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ); + } elseif ( $order->get_shipping_total() > 0 ) { + $this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) ); + } + + $line_item_args = array_merge( $line_item_args, $this->get_line_items() ); + } + + return $line_item_args; + } + + /** + * Get order item names as a string. + * @param WC_Order $order + * @return string + */ + protected function get_order_item_names( $order ) { + $item_names = array(); + + foreach ( $order->get_items() as $item ) { + $item_name = $item->get_name(); + $item_meta = strip_tags( wc_display_item_meta( $item, array( + 'before' => "", + 'separator' => ", ", + 'after' => "", + 'echo' => false, + 'autop' => false, + ) ) ); + + if ( $item_meta ) { + $item_name .= ' (' . $item_meta . ')'; + } + + $item_names[] = $item_name . ' x ' . $item->get_quantity(); + } + + return apply_filters( 'woocommerce_paypal_get_order_item_names', implode( ', ', $item_names ), $order ); + } + + /** + * Get order item names as a string. + * @param WC_Order $order + * @param array $item + * @return string + */ + protected function get_order_item_name( $order, $item ) { + $item_name = $item->get_name(); + $item_meta = strip_tags( wc_display_item_meta( $item, array( + 'before' => "", + 'separator' => ", ", + 'after' => "", + 'echo' => false, + 'autop' => false, + ) ) ); + + if ( $item_meta ) { + $item_name .= ' (' . $item_meta . ')'; + } + + return apply_filters( 'woocommerce_paypal_get_order_item_name', $item_name, $order, $item ); + } + + /** + * Return all line items. + */ + protected function get_line_items() { + return $this->line_items; + } + + /** + * Remove all line items. + */ + protected function delete_line_items() { + $this->line_items = array(); + } + + /** + * Get line items to send to paypal. + * @param WC_Order $order + * @return bool + */ + protected function prepare_line_items( $order ) { + $this->delete_line_items(); + $calculated_total = 0; + + // Products + foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) { + if ( 'fee' === $item['type'] ) { + $item_line_total = $this->number_format( $item['line_total'], $order ); + $line_item = $this->add_line_item( $item->get_name(), 1, $item_line_total ); + $calculated_total += $item_line_total; + } else { + $product = $item->get_product(); + $sku = $product ? $product->get_sku() : ''; + $item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order ); + $line_item = $this->add_line_item( $this->get_order_item_name( $order, $item ), $item->get_quantity(), $item_line_total, $sku ); + $calculated_total += $item_line_total * $item->get_quantity(); + } + + if ( ! $line_item ) { + return false; + } + } + + // Check for mismatched totals. + if ( $this->number_format( $calculated_total + $order->get_total_tax() + $this->round( $order->get_shipping_total(), $order ) - $this->round( $order->get_total_discount(), $order ), $order ) != $this->number_format( $order->get_total(), $order ) ) { + return false; + } + + return true; + } + + /** + * Add PayPal Line Item. + * @param string $item_name + * @param int $quantity + * @param float $amount + * @param string $item_number + * @return bool successfully added or not + */ + protected function add_line_item( $item_name, $quantity = 1, $amount = 0.0, $item_number = '' ) { + $index = ( sizeof( $this->line_items ) / 4 ) + 1; + + if ( $amount < 0 || $index > 9 ) { + return false; + } + + $item = apply_filters( 'woocommerce_paypal_line_item', array( + 'item_name' => html_entity_decode( wc_trim_string( $item_name ? $item_name : __( 'Item', 'woocommerce' ), 127 ), ENT_NOQUOTES, 'UTF-8' ), + 'quantity' => (int) $quantity, + 'amount' => wc_float_to_string( (float) $amount ), + 'item_number' => $item_number, + ), $item_name, $quantity, $amount, $item_number ); + + $this->line_items[ 'item_name_' . $index ] = $this->limit_length( $item['item_name'], 127 ); + $this->line_items[ 'quantity_' . $index ] = $item['quantity']; + $this->line_items[ 'amount_' . $index ] = $item['amount']; + $this->line_items[ 'item_number_' . $index ] = $this->limit_length( $item['item_number'], 127 ); + + return true; + } + + /** + * Get the state to send to paypal. + * @param string $cc + * @param string $state + * @return string + */ + protected function get_paypal_state( $cc, $state ) { + if ( 'US' === $cc ) { + return $state; + } + + $states = WC()->countries->get_states( $cc ); + + if ( isset( $states[ $state ] ) ) { + return $states[ $state ]; + } + + return $state; + } + + /** + * Check if currency has decimals. + * @param string $currency + * @return bool + */ + protected function currency_has_decimals( $currency ) { + if ( in_array( $currency, array( 'HUF', 'JPY', 'TWD' ) ) ) { + return false; + } + + return true; + } + + /** + * Round prices. + * @param double $price + * @param WC_Order $order + * @return double + */ + protected function round( $price, $order ) { + $precision = 2; + + if ( ! $this->currency_has_decimals( $order->get_currency() ) ) { + $precision = 0; + } + + return round( $price, $precision ); + } + + /** + * Format prices. + * @param float|int $price + * @param WC_Order $order + * @return string + */ + protected function number_format( $price, $order ) { + $decimals = 2; + + if ( ! $this->currency_has_decimals( $order->get_currency() ) ) { + $decimals = 0; + } + + return number_format( $price, $decimals, '.', '' ); + } +} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php new file mode 100644 index 00000000000..b543f362736 --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php @@ -0,0 +1,72 @@ +order_id; + $order_key = $custom->order_key; + + // Fallback to serialized data if safe. This is @deprecated in 2.3.11 + } elseif ( preg_match( '/^a:2:{/', $raw_custom ) && ! preg_match( '/[CO]:\+?[0-9]+:"/', $raw_custom ) && ( $custom = maybe_unserialize( $raw_custom ) ) ) { + $order_id = $custom[0]; + $order_key = $custom[1]; + + // Nothing was found. + } else { + WC_Gateway_Paypal::log( 'Order ID and key were not found in "custom".', 'error' ); + return false; + } + + if ( ! $order = wc_get_order( $order_id ) ) { + // We have an invalid $order_id, probably because invoice_prefix has changed. + $order_id = wc_get_order_id_by_order_key( $order_key ); + $order = wc_get_order( $order_id ); + } + + if ( ! $order || $order->get_order_key() !== $order_key ) { + WC_Gateway_Paypal::log( 'Order Keys do not match.', 'error' ); + return false; + } + + return $order; + } + + /** + * Complete order, add transaction ID and note. + * @param WC_Order $order + * @param string $txn_id + * @param string $note + */ + protected function payment_complete( $order, $txn_id = '', $note = '' ) { + $order->add_order_note( $note ); + $order->payment_complete( $txn_id ); + } + + /** + * Hold order and add note. + * @param WC_Order $order + * @param string $reason + */ + protected function payment_on_hold( $order, $reason = '' ) { + $order->update_status( 'on-hold', $reason ); + wc_reduce_stock_levels( $order->get_id() ); + WC()->cart->empty_cart(); + } +} diff --git a/includes/gateways/paypal/includes/settings-paypal.php b/includes/gateways/paypal/includes/settings-paypal.php new file mode 100644 index 00000000000..693af77bf41 --- /dev/null +++ b/includes/gateways/paypal/includes/settings-paypal.php @@ -0,0 +1,152 @@ + array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal Standard', 'woocommerce' ), + 'default' => 'yes', + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'PayPal', 'woocommerce' ), + 'desc_tip' => true, + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ), + 'default' => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", 'woocommerce' ), + ), + 'email' => array( + 'title' => __( 'PayPal email', 'woocommerce' ), + 'type' => 'email', + 'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ), + 'default' => get_option( 'admin_email' ), + 'desc_tip' => true, + 'placeholder' => 'you@youremail.com', + ), + 'testmode' => array( + 'title' => __( 'PayPal sandbox', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal sandbox', 'woocommerce' ), + 'default' => 'no', + 'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a developer account.', 'woocommerce' ), 'https://developer.paypal.com/' ), + ), + 'debug' => array( + 'title' => __( 'Debug log', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable logging', 'woocommerce' ), + 'default' => 'no', + 'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s', 'woocommerce' ), '' . WC_Log_Handler_File::get_log_file_path( 'paypal' ) . '' ), + ), + 'advanced' => array( + 'title' => __( 'Advanced options', 'woocommerce' ), + 'type' => 'title', + 'description' => '', + ), + 'receiver_email' => array( + 'title' => __( 'Receiver email', 'woocommerce' ), + 'type' => 'email', + 'description' => __( 'If your main PayPal email differs from the PayPal email entered above, input your main receiver email for your PayPal account here. This is used to validate IPN requests.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => 'you@youremail.com', + ), + 'identity_token' => array( + 'title' => __( 'PayPal identity token', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Profile and Settings > My Selling Tools > Website Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => '', + ), + 'invoice_prefix' => array( + 'title' => __( 'Invoice prefix', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ), + 'default' => 'WC-', + 'desc_tip' => true, + ), + 'send_shipping' => array( + 'title' => __( 'Shipping details', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ), + 'description' => __( 'PayPal allows us to send one address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing.', 'woocommerce' ), + 'default' => 'no', + ), + 'address_override' => array( + 'title' => __( 'Address override', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ), + 'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ), + 'default' => 'no', + ), + 'paymentaction' => array( + 'title' => __( 'Payment action', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ), + 'default' => 'sale', + 'desc_tip' => true, + 'options' => array( + 'sale' => __( 'Capture', 'woocommerce' ), + 'authorization' => __( 'Authorize', 'woocommerce' ), + ), + ), + 'page_style' => array( + 'title' => __( 'Page style', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Optionally enter the name of the page style you wish to use. These are defined within your PayPal account. This affects classic PayPal checkout screens.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'image_url' => array( + 'title' => __( 'Image url', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Optionally enter the URL to a 150x50px image displayed as your logo in the upper left corner of the PayPal checkout pages.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_details' => array( + 'title' => __( 'API credentials', 'woocommerce' ), + 'type' => 'title', + 'description' => sprintf( __( 'Enter your PayPal API credentials to process refunds via PayPal. Learn how to access your PayPal API Credentials.', 'woocommerce' ), 'https://developer.paypal.com/webapps/developer/docs/classic/api/apiCredentials/#creating-an-api-signature' ), + ), + 'api_username' => array( + 'title' => __( 'API username', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_password' => array( + 'title' => __( 'API password', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_signature' => array( + 'title' => __( 'API signature', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), +); diff --git a/includes/gateways/simplify-commerce/assets/images/cards.png b/includes/gateways/simplify-commerce/assets/images/cards.png new file mode 100644 index 00000000000..c4091a40060 Binary files /dev/null and b/includes/gateways/simplify-commerce/assets/images/cards.png differ diff --git a/includes/gateways/simplify-commerce/assets/images/logo.png b/includes/gateways/simplify-commerce/assets/images/logo.png new file mode 100644 index 00000000000..47e0653885f Binary files /dev/null and b/includes/gateways/simplify-commerce/assets/images/logo.png differ diff --git a/includes/gateways/simplify-commerce/assets/js/simplify-commerce.js b/includes/gateways/simplify-commerce/assets/js/simplify-commerce.js new file mode 100644 index 00000000000..88aafb49fb6 --- /dev/null +++ b/includes/gateways/simplify-commerce/assets/js/simplify-commerce.js @@ -0,0 +1,118 @@ +/*global Simplify_commerce_params, SimplifyCommerce */ +(function ( $ ) { + + // Form handler + function simplifyFormHandler() { + var $form = $( 'form.checkout, form#order_review, form#add_payment_method' ); + + if ( ( $( '#payment_method_simplify_commerce' ).is( ':checked' ) && 'new' === $( 'input[name="wc-simplify_commerce-payment-token"]:checked' ).val() ) || ( '1' === $( '#woocommerce_add_payment_method' ).val() ) ) { + + if ( 0 === $( 'input.simplify-token' ).length ) { + + $form.block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var card = $( '#simplify_commerce-card-number' ).val(), + cvc = $( '#simplify_commerce-card-cvc' ).val(), + expiry = $.payment.cardExpiryVal( $( '#simplify_commerce-card-expiry' ).val() ), + address1 = $form.find( '#billing_address_1' ).val() || '', + address2 = $form.find( '#billing_address_2' ).val() || '', + addressCountry = $form.find( '#billing_country' ).val() || '', + addressState = $form.find( '#billing_state' ).val() || '', + addressCity = $form.find( '#billing_city' ).val() || '', + addressZip = $form.find( '#billing_postcode' ).val() || ''; + + addressZip = addressZip.replace( /-/g, '' ); + card = card.replace( /\s/g, '' ); + + SimplifyCommerce.generateToken({ + key: Simplify_commerce_params.key, + card: { + number: card, + cvc: cvc, + expMonth: expiry.month, + expYear: ( expiry.year - 2000 ), + addressLine1: address1, + addressLine2: address2, + addressCountry: addressCountry, + addressState: addressState, + addressZip: addressZip, + addressCity: addressCity + } + }, simplifyResponseHandler ); + + // Prevent the form from submitting + return false; + } + } + + return true; + } + + // Handle Simplify response + function simplifyResponseHandler( data ) { + + var $form = $( 'form.checkout, form#order_review, form#add_payment_method' ), + ccForm = $( '#wc-simplify_commerce-cc-form' ); + + if ( data.error ) { + + // Show the errors on the form + $( '.woocommerce-error, .simplify-token', ccForm ).remove(); + $form.unblock(); + + // Show any validation errors + if ( 'validation' === data.error.code ) { + var fieldErrors = data.error.fieldErrors, + fieldErrorsLength = fieldErrors.length, + errorList = ''; + + for ( var i = 0; i < fieldErrorsLength; i++ ) { + errorList += '
  • ' + Simplify_commerce_params[ fieldErrors[i].field ] + ' ' + Simplify_commerce_params.is_invalid + ' - ' + fieldErrors[i].message + '.
  • '; + } + + ccForm.prepend( '
      ' + errorList + '
    ' ); + } + + } else { + + // Insert the token into the form so it gets submitted to the server + ccForm.append( '' ); + $form.submit(); + } + } + + $( function () { + + $( document.body ).on( 'checkout_error', function () { + $( '.simplify-token' ).remove(); + }); + + /* Checkout Form */ + $( 'form.checkout' ).on( 'checkout_place_order_simplify_commerce', function () { + return simplifyFormHandler(); + }); + + /* Pay Page Form */ + $( 'form#order_review' ).on( 'submit', function () { + return simplifyFormHandler(); + }); + + /* Pay Page Form */ + $( 'form#add_payment_method' ).on( 'submit', function () { + return simplifyFormHandler(); + }); + + /* Both Forms */ + $( 'form.checkout, form#order_review, form#add_payment_method' ).on( 'change', '#wc-simplify_commerce-cc-form input', function() { + $( '.simplify-token' ).remove(); + }); + + }); + +}( jQuery ) ); diff --git a/includes/gateways/simplify-commerce/assets/js/simplify-commerce.min.js b/includes/gateways/simplify-commerce/assets/js/simplify-commerce.min.js new file mode 100644 index 00000000000..9491d4a152f --- /dev/null +++ b/includes/gateways/simplify-commerce/assets/js/simplify-commerce.min.js @@ -0,0 +1 @@ +!function(e){function r(){var r=e("form.checkout, form#order_review, form#add_payment_method");if((e("#payment_method_simplify_commerce").is(":checked")&&"new"===e('input[name="wc-simplify_commerce-payment-token"]:checked').val()||"1"===e("#woocommerce_add_payment_method").val())&&0===e("input.simplify-token").length){r.block({message:null,overlayCSS:{background:"#fff",opacity:.6}});var i=e("#simplify_commerce-card-number").val(),m=e("#simplify_commerce-card-cvc").val(),c=e.payment.cardExpiryVal(e("#simplify_commerce-card-expiry").val()),n=r.find("#billing_address_1").val()||"",a=r.find("#billing_address_2").val()||"",t=r.find("#billing_country").val()||"",d=r.find("#billing_state").val()||"",l=r.find("#billing_city").val()||"",f=r.find("#billing_postcode").val()||"";return f=f.replace(/-/g,""),i=i.replace(/\s/g,""),SimplifyCommerce.generateToken({key:Simplify_commerce_params.key,card:{number:i,cvc:m,expMonth:c.month,expYear:c.year-2e3,addressLine1:n,addressLine2:a,addressCountry:t,addressState:d,addressZip:f,addressCity:l}},o),!1}return!0}function o(r){var o=e("form.checkout, form#order_review, form#add_payment_method"),i=e("#wc-simplify_commerce-cc-form");if(r.error){if(e(".woocommerce-error, .simplify-token",i).remove(),o.unblock(),"validation"===r.error.code){for(var m=r.error.fieldErrors,c=m.length,n="",a=0;a"+Simplify_commerce_params[m[a].field]+" "+Simplify_commerce_params.is_invalid+" - "+m[a].message+".";i.prepend('
      '+n+"
    ")}}else i.append(''),o.submit()}e(function(){e(document.body).on("checkout_error",function(){e(".simplify-token").remove()}),e("form.checkout").on("checkout_place_order_simplify_commerce",function(){return r()}),e("form#order_review").on("submit",function(){return r()}),e("form#add_payment_method").on("submit",function(){return r()}),e("form.checkout, form#order_review, form#add_payment_method").on("change","#wc-simplify_commerce-cc-form input",function(){e(".simplify-token").remove()})})}(jQuery); \ No newline at end of file diff --git a/includes/gateways/simplify-commerce/class-wc-addons-gateway-simplify-commerce.php b/includes/gateways/simplify-commerce/class-wc-addons-gateway-simplify-commerce.php new file mode 100644 index 00000000000..396bfd9d530 --- /dev/null +++ b/includes/gateways/simplify-commerce/class-wc-addons-gateway-simplify-commerce.php @@ -0,0 +1,517 @@ +id, array( $this, 'scheduled_subscription_payment' ), 10, 2 ); + add_action( 'woocommerce_subscription_failing_payment_method_updated_' . $this->id, array( $this, 'update_failing_payment_method' ), 10, 2 ); + + add_action( 'wcs_resubscribe_order_created', array( $this, 'delete_resubscribe_meta' ), 10 ); + + // Allow store managers to manually set Simplify as the payment method on a subscription + add_filter( 'woocommerce_subscription_payment_meta', array( $this, 'add_subscription_payment_meta' ), 10, 2 ); + add_filter( 'woocommerce_subscription_validate_payment_meta', array( $this, 'validate_subscription_payment_meta' ), 10, 2 ); + } + + if ( class_exists( 'WC_Pre_Orders_Order' ) ) { + add_action( 'wc_pre_orders_process_pre_order_completion_payment_' . $this->id, array( $this, 'process_pre_order_release_payment' ) ); + } + + add_filter( 'woocommerce_simplify_commerce_hosted_args', array( $this, 'hosted_payment_args' ), 10, 2 ); + add_action( 'woocommerce_api_wc_addons_gateway_simplify_commerce', array( $this, 'return_handler' ) ); + } + + /** + * Hosted payment args. + * + * @param array $args + * @param int $order_id + * @return array + */ + public function hosted_payment_args( $args, $order_id ) { + if ( ( $this->order_contains_subscription( $order_id ) ) || ( $this->order_contains_pre_order( $order_id ) && WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) ) ) { + $args['operation'] = 'create.token'; + } + + $args['redirect-url'] = WC()->api_request_url( 'WC_Addons_Gateway_Simplify_Commerce' ); + + return $args; + } + + /** + * Check if order contains subscriptions. + * + * @param int $order_id + * @return bool + */ + protected function order_contains_subscription( $order_id ) { + return function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ); + } + + /** + * Check if order contains pre-orders. + * + * @param int $order_id + * @return bool + */ + protected function order_contains_pre_order( $order_id ) { + return class_exists( 'WC_Pre_Orders_Order' ) && WC_Pre_Orders_Order::order_contains_pre_order( $order_id ); + } + + /** + * Process the subscription. + * + * @param WC_Order $order + * @param string $cart_token + * @uses Simplify_ApiException + * @uses Simplify_BadRequestException + * @return array + * @throws Exception + */ + protected function process_subscription( $order, $cart_token = '' ) { + try { + if ( empty( $cart_token ) ) { + $error_msg = __( 'Please make sure your card details have been entered correctly and that your browser supports JavaScript.', 'woocommerce' ); + + if ( 'yes' == $this->sandbox ) { + $error_msg .= ' ' . __( 'Developers: Please make sure that you are including jQuery and there are no JavaScript errors on the page.', 'woocommerce' ); + } + + throw new Simplify_ApiException( $error_msg ); + } + + // Create customer + $customer = Simplify_Customer::createCustomer( array( + 'token' => $cart_token, + 'email' => $order->get_billing_email(), + 'name' => trim( $order->get_formatted_billing_full_name() ), + 'reference' => $order->get_id(), + ) ); + + if ( is_object( $customer ) && '' != $customer->id ) { + $this->save_subscription_meta( $order->get_id(), $customer->id ); + } else { + $error_msg = __( 'Error creating user in Simplify Commerce.', 'woocommerce' ); + + throw new Simplify_ApiException( $error_msg ); + } + + $payment_response = $this->process_subscription_payment( $order, $order->get_total() ); + + if ( is_wp_error( $payment_response ) ) { + throw new Exception( $payment_response->get_error_message() ); + } else { + // Remove cart + WC()->cart->empty_cart(); + + // Return thank you page redirect + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $order ), + ); + } + } catch ( Simplify_ApiException $e ) { + if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { + foreach ( $e->getFieldErrors() as $error ) { + wc_add_notice( $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')', 'error' ); + } + } else { + wc_add_notice( $e->getMessage(), 'error' ); + } + + return array( + 'result' => 'fail', + 'redirect' => '', + ); + } + } + + /** + * Store the customer and card IDs on the order and subscriptions in the order. + * + * @param int $order_id + * @param string $customer_id + */ + protected function save_subscription_meta( $order_id, $customer_id ) { + + $customer_id = wc_clean( $customer_id ); + + update_post_meta( $order_id, '_simplify_customer_id', $customer_id ); + + // Also store it on the subscriptions being purchased in the order + foreach ( wcs_get_subscriptions_for_order( $order_id ) as $subscription ) { + update_post_meta( $subscription->id, '_simplify_customer_id', $customer_id ); + } + } + + /** + * Process the pre-order. + * + * @param WC_Order $order + * @param string $cart_token + * @uses Simplify_ApiException + * @uses Simplify_BadRequestException + * @return array + */ + protected function process_pre_order( $order, $cart_token = '' ) { + if ( WC_Pre_Orders_Order::order_requires_payment_tokenization( $order->get_id() ) ) { + + try { + if ( $order->get_total() * 100 < 50 ) { + $error_msg = __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce' ); + + throw new Simplify_ApiException( $error_msg ); + } + + if ( empty( $cart_token ) ) { + $error_msg = __( 'Please make sure your card details have been entered correctly and that your browser supports JavaScript.', 'woocommerce' ); + + if ( 'yes' == $this->sandbox ) { + $error_msg .= ' ' . __( 'Developers: Please make sure that you are including jQuery and there are no JavaScript errors on the page.', 'woocommerce' ); + } + + throw new Simplify_ApiException( $error_msg ); + } + + // Create customer + $customer = Simplify_Customer::createCustomer( array( + 'token' => $cart_token, + 'email' => $order->get_billing_email(), + 'name' => trim( $order->get_formatted_billing_full_name() ), + 'reference' => $order->get_id(), + ) ); + + if ( is_object( $customer ) && '' != $customer->id ) { + $customer_id = wc_clean( $customer->id ); + + // Store the customer ID in the order + update_post_meta( $order->get_id(), '_simplify_customer_id', $customer_id ); + } else { + $error_msg = __( 'Error creating user in Simplify Commerce.', 'woocommerce' ); + + throw new Simplify_ApiException( $error_msg ); + } + + // Reduce stock levels + wc_reduce_stock_levels( $order->get_id() ); + + // Remove cart + WC()->cart->empty_cart(); + + // Is pre ordered! + WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order ); + + // Return thank you page redirect + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $order ), + ); + + } catch ( Simplify_ApiException $e ) { + if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { + foreach ( $e->getFieldErrors() as $error ) { + wc_add_notice( $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')', 'error' ); + } + } else { + wc_add_notice( $e->getMessage(), 'error' ); + } + + return array( + 'result' => 'fail', + 'redirect' => '', + ); + } + + } else { + return parent::process_standard_payments( $order, $cart_token ); + } + } + + /** + * Process the payment. + * + * @param int $order_id + * @return array + */ + public function process_payment( $order_id ) { + $cart_token = isset( $_POST['simplify_token'] ) ? wc_clean( $_POST['simplify_token'] ) : ''; + $order = wc_get_order( $order_id ); + + // Processing subscription + if ( 'standard' == $this->mode && ( $this->order_contains_subscription( $order->get_id() ) || ( function_exists( 'wcs_is_subscription' ) && wcs_is_subscription( $order_id ) ) ) ) { + return $this->process_subscription( $order, $cart_token ); + + // Processing pre-order + } elseif ( 'standard' == $this->mode && $this->order_contains_pre_order( $order->get_id() ) ) { + return $this->process_pre_order( $order, $cart_token ); + + // Processing regular product + } else { + return parent::process_payment( $order_id ); + } + } + + /** + * process_subscription_payment function. + * + * @param WC_order $order + * @param int $amount (default: 0) + * @uses Simplify_BadRequestException + * @return bool|WP_Error + */ + public function process_subscription_payment( $order, $amount = 0 ) { + if ( 0 == $amount ) { + // Payment complete + $order->payment_complete(); + + return true; + } + + if ( $amount * 100 < 50 ) { + return new WP_Error( 'simplify_error', __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce' ) ); + } + + $customer_id = get_post_meta( $order->get_id(), '_simplify_customer_id', true ); + + if ( ! $customer_id ) { + return new WP_Error( 'simplify_error', __( 'Customer not found.', 'woocommerce' ) ); + } + + try { + // Charge the customer + $payment = Simplify_Payment::createPayment( array( + 'amount' => $amount * 100, // In cents. + 'customer' => $customer_id, + 'description' => sprintf( __( '%1$s - Order #%2$s', 'woocommerce' ), esc_html( get_bloginfo( 'name', 'display' ) ), $order->get_order_number() ), + 'currency' => strtoupper( get_woocommerce_currency() ), + 'reference' => $order->get_id(), + ) ); + + } catch ( Exception $e ) { + + $error_message = $e->getMessage(); + + if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { + $error_message = ''; + foreach ( $e->getFieldErrors() as $error ) { + $error_message .= ' ' . $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')'; + } + } + + $order->add_order_note( sprintf( __( 'Simplify payment error: %s.', 'woocommerce' ), $error_message ) ); + + return new WP_Error( 'simplify_payment_declined', $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + if ( 'APPROVED' == $payment->paymentStatus ) { + // Payment complete + $order->payment_complete( $payment->id ); + + // Add order note + $order->add_order_note( sprintf( __( 'Simplify payment approved (ID: %1$s, Auth Code: %2$s)', 'woocommerce' ), $payment->id, $payment->authCode ) ); + + return true; + } else { + $order->add_order_note( __( 'Simplify payment declined', 'woocommerce' ) ); + + return new WP_Error( 'simplify_payment_declined', __( 'Payment was declined - please try another card.', 'woocommerce' ) ); + } + } + + /** + * scheduled_subscription_payment function. + * + * @param float $amount_to_charge The amount to charge. + * @param WC_Order $renewal_order A WC_Order object created to record the renewal payment. + */ + public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) { + $result = $this->process_subscription_payment( $renewal_order, $amount_to_charge ); + + if ( is_wp_error( $result ) ) { + $renewal_order->update_status( 'failed', sprintf( __( 'Simplify Transaction Failed (%s)', 'woocommerce' ), $result->get_error_message() ) ); + } + } + + /** + * Update the customer_id for a subscription after using Simplify to complete a payment to make up for. + * an automatic renewal payment which previously failed. + * + * @param WC_Subscription $subscription The subscription for which the failing payment method relates. + * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment). + */ + public function update_failing_payment_method( $subscription, $renewal_order ) { + update_post_meta( $subscription->id, '_simplify_customer_id', get_post_meta( $renewal_order->get_id(), '_simplify_customer_id', true ) ); + } + + /** + * Include the payment meta data required to process automatic recurring payments so that store managers can. + * manually set up automatic recurring payments for a customer via the Edit Subscription screen in Subscriptions v2.0+. + * + * @since 2.4 + * @param array $payment_meta associative array of meta data required for automatic payments + * @param WC_Subscription $subscription An instance of a subscription object + * @return array + */ + public function add_subscription_payment_meta( $payment_meta, $subscription ) { + + $payment_meta[ $this->id ] = array( + 'post_meta' => array( + '_simplify_customer_id' => array( + 'value' => get_post_meta( $subscription->id, '_simplify_customer_id', true ), + 'label' => 'Simplify Customer ID', + ), + ), + ); + + return $payment_meta; + } + + /** + * Validate the payment meta data required to process automatic recurring payments so that store managers can. + * manually set up automatic recurring payments for a customer via the Edit Subscription screen in Subscriptions 2.0+. + * + * @since 2.4 + * @param string $payment_method_id The ID of the payment method to validate + * @param array $payment_meta associative array of meta data required for automatic payments + * @return array + * @throws Exception + */ + public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ) { + if ( $this->id === $payment_method_id ) { + if ( ! isset( $payment_meta['post_meta']['_simplify_customer_id']['value'] ) || empty( $payment_meta['post_meta']['_simplify_customer_id']['value'] ) ) { + throw new Exception( 'A "_simplify_customer_id" value is required.' ); + } + } + } + + /** + * Don't transfer customer meta to resubscribe orders. + * + * @access public + * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription + * @return void + */ + public function delete_resubscribe_meta( $resubscribe_order ) { + delete_post_meta( $resubscribe_order->get_id(), '_simplify_customer_id' ); + } + + /** + * Process a pre-order payment when the pre-order is released. + * + * @param WC_Order $order + * @return WP_Error|null + */ + public function process_pre_order_release_payment( $order ) { + + try { + $order_items = $order->get_items(); + $order_item = array_shift( $order_items ); + /* translators: 1: site name 2: product name 3: order number */ + $pre_order_name = sprintf( + __( '%1$s - Pre-order for "%2$s" (Order #%3$s)', 'woocommerce' ), + esc_html( get_bloginfo( 'name', 'display' ) ), + $order_item['name'], + $order->get_order_number() + ); + + $customer_id = get_post_meta( $order->get_id(), '_simplify_customer_id', true ); + + if ( ! $customer_id ) { + return new WP_Error( 'simplify_error', __( 'Customer not found.', 'woocommerce' ) ); + } + + // Charge the customer + $payment = Simplify_Payment::createPayment( array( + 'amount' => $order->get_total() * 100, // In cents. + 'customer' => $customer_id, + 'description' => trim( substr( $pre_order_name, 0, 1024 ) ), + 'currency' => strtoupper( get_woocommerce_currency() ), + 'reference' => $order->get_id(), + ) ); + + if ( 'APPROVED' == $payment->paymentStatus ) { + // Payment complete + $order->payment_complete( $payment->id ); + + // Add order note + $order->add_order_note( sprintf( __( 'Simplify payment approved (ID: %1$s, Auth Code: %2$s)', 'woocommerce' ), $payment->id, $payment->authCode ) ); + } else { + return new WP_Error( 'simplify_payment_declined', __( 'Payment was declined - the customer need to try another card.', 'woocommerce' ) ); + } + } catch ( Exception $e ) { + $order_note = sprintf( __( 'Simplify Transaction Failed (%s)', 'woocommerce' ), $e->getMessage() ); + + // Mark order as failed if not already set, + // otherwise, make sure we add the order note so we can detect when someone fails to check out multiple times + if ( 'failed' != $order->get_status() ) { + $order->update_status( 'failed', $order_note ); + } else { + $order->add_order_note( $order_note ); + } + } + } + + /** + * Return handler for Hosted Payments. + */ + public function return_handler() { + if ( ! isset( $_REQUEST['cardToken'] ) ) { + parent::return_handler(); + } + + @ob_clean(); + header( 'HTTP/1.1 200 OK' ); + + $redirect_url = wc_get_page_permalink( 'cart' ); + + if ( isset( $_REQUEST['reference'] ) && isset( $_REQUEST['amount'] ) ) { + $cart_token = $_REQUEST['cardToken']; + $amount = absint( $_REQUEST['amount'] ); + $order_id = absint( $_REQUEST['reference'] ); + $order = wc_get_order( $order_id ); + $order_total = absint( $order->get_total() * 100 ); + + if ( $amount === $order_total ) { + if ( $this->order_contains_subscription( $order->get_id() ) ) { + $response = $this->process_subscription( $order, $cart_token ); + } elseif ( $this->order_contains_pre_order( $order->get_id() ) ) { + $response = $this->process_pre_order( $order, $cart_token ); + } else { + $response = parent::process_standard_payments( $order, $cart_token ); + } + + if ( 'success' == $response['result'] ) { + $redirect_url = $response['redirect']; + } else { + $order->update_status( 'failed', __( 'Payment was declined by Simplify Commerce.', 'woocommerce' ) ); + } + + wp_redirect( $redirect_url ); + exit(); + } + } + + wp_redirect( $redirect_url ); + exit(); + } +} diff --git a/includes/gateways/simplify-commerce/class-wc-gateway-simplify-commerce.php b/includes/gateways/simplify-commerce/class-wc-gateway-simplify-commerce.php new file mode 100644 index 00000000000..1fd982ad642 --- /dev/null +++ b/includes/gateways/simplify-commerce/class-wc-gateway-simplify-commerce.php @@ -0,0 +1,769 @@ +id = 'simplify_commerce'; + $this->method_title = __( 'Simplify Commerce', 'woocommerce' ); + $this->method_description = __( 'Take payments via Simplify Commerce - uses simplify.js to create card tokens and the Simplify Commerce SDK. Requires SSL when sandbox is disabled.', 'woocommerce' ); + $this->new_method_label = __( 'Use a new card', 'woocommerce' ); + $this->has_fields = true; + $this->supports = array( + 'subscriptions', + 'products', + 'subscription_cancellation', + 'subscription_reactivation', + 'subscription_suspension', + 'subscription_amount_changes', + 'subscription_payment_method_change', // Subscriptions 1.n compatibility + 'subscription_payment_method_change_customer', + 'subscription_payment_method_change_admin', + 'subscription_date_changes', + 'multiple_subscriptions', + 'default_credit_card_form', + 'tokenization', + 'refunds', + 'pre-orders', + ); + $this->view_transaction_url = 'https://www.simplify.com/commerce/app#/payment/%s'; + + // Load the form fields + $this->init_form_fields(); + + // Load the settings. + $this->init_settings(); + + // Get setting values + $this->title = $this->get_option( 'title' ); + $this->description = $this->get_option( 'description' ); + $this->enabled = $this->get_option( 'enabled' ); + $this->mode = $this->get_option( 'mode', 'standard' ); + $this->modal_color = $this->get_option( 'modal_color', '#a46497' ); + $this->sandbox = $this->get_option( 'sandbox' ); + $this->public_key = ( 'no' === $this->sandbox ) ? $this->get_option( 'public_key' ) : $this->get_option( 'sandbox_public_key' ); + $this->private_key = ( 'no' === $this->sandbox ) ? $this->get_option( 'private_key' ) : $this->get_option( 'sandbox_private_key' ); + + $this->init_simplify_sdk(); + + // Hooks + add_action( 'wp_enqueue_scripts', array( $this, 'payment_scripts' ) ); + add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); + add_action( 'woocommerce_receipt_' . $this->id, array( $this, 'receipt_page' ) ); + add_action( 'woocommerce_api_wc_gateway_simplify_commerce', array( $this, 'return_handler' ) ); + } + + /** + * Init Simplify SDK. + */ + protected function init_simplify_sdk() { + // Include lib + require_once( dirname( __FILE__ ) . '/includes/Simplify.php' ); + + Simplify::$publicKey = $this->public_key; + Simplify::$privateKey = $this->private_key; + Simplify::$userAgent = 'WooCommerce/' . WC()->version; + } + + /** + * Admin Panel Options. + * - Options for bits like 'title' and availability on a country-by-country basis. + */ + public function admin_options() { + ?> +

    + + public_key ) ) : ?> +
    + +

    +

    + +

    + +
    + +

    + + + checks(); ?> + + + generate_settings_html(); ?> + +
    + enabled ) { + return; + } + + if ( version_compare( phpversion(), '5.3', '<' ) ) { + + // PHP Version + echo '

    ' . sprintf( __( 'Simplify Commerce Error: Simplify commerce requires PHP 5.3 and above. You are using version %s.', 'woocommerce' ), phpversion() ) . '

    '; + + } elseif ( ! $this->public_key || ! $this->private_key ) { + + // Check required fields + echo '

    ' . __( 'Simplify Commerce Error: Please enter your public and private keys', 'woocommerce' ) . '

    '; + + } elseif ( 'standard' == $this->mode && ! wc_checkout_is_https() ) { + + // Show message when using standard mode and no SSL on the checkout page + echo '

    ' . sprintf( __( 'Simplify Commerce is enabled, but the force SSL option is disabled; your checkout may not be secure! Please enable SSL and ensure your server has a valid SSL certificate - Simplify Commerce will only work in sandbox mode.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=checkout' ) ) . '

    '; + + } + } + + /** + * Check if this gateway is enabled. + * + * @return bool + */ + public function is_available() { + if ( 'yes' !== $this->enabled ) { + return false; + } + + if ( 'standard' === $this->mode && 'yes' !== $this->sandbox && ! wc_checkout_is_https() ) { + return false; + } + + if ( ! $this->public_key || ! $this->private_key ) { + return false; + } + + return true; + } + + /** + * Initialise Gateway Settings Form Fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'label' => __( 'Enable Simplify Commerce', 'woocommerce' ), + 'type' => 'checkbox', + 'description' => '', + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'Credit card', 'woocommerce' ), + 'desc_tip' => true, + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ), + 'default' => 'Pay with your credit card via Simplify Commerce by MasterCard.', + 'desc_tip' => true, + ), + 'mode' => array( + 'title' => __( 'Payment mode', 'woocommerce' ), + 'label' => __( 'Enable Hosted Payments', 'woocommerce' ), + 'type' => 'select', + 'description' => sprintf( __( 'Standard will display the credit card fields on your store (SSL required). %1$s Hosted Payments will display a Simplify Commerce modal dialog on your store (if SSL) or will redirect the customer to Simplify Commerce hosted page (if not SSL). %1$s Note: Hosted Payments need a new API Key pair with the hosted payments flag selected. %2$sFor more details check the Simplify Commerce docs%3$s.', 'woocommerce' ), '
    ', '', '' ), + 'default' => 'standard', + 'options' => array( + 'standard' => __( 'Standard', 'woocommerce' ), + 'hosted' => __( 'Hosted Payments', 'woocommerce' ), + ), + ), + 'modal_color' => array( + 'title' => __( 'Modal color', 'woocommerce' ), + 'type' => 'color', + 'description' => __( 'Set the color of the buttons and titles on the modal dialog.', 'woocommerce' ), + 'default' => '#a46497', + 'desc_tip' => true, + ), + 'sandbox' => array( + 'title' => __( 'Sandbox', 'woocommerce' ), + 'label' => __( 'Enable sandbox mode', 'woocommerce' ), + 'type' => 'checkbox', + 'description' => __( 'Place the payment gateway in sandbox mode using sandbox API keys (real payments will not be taken).', 'woocommerce' ), + 'default' => 'yes', + ), + 'sandbox_public_key' => array( + 'title' => __( 'Sandbox public key', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API keys from your Simplify account: Settings > API Keys.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + ), + 'sandbox_private_key' => array( + 'title' => __( 'Sandbox private key', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API keys from your Simplify account: Settings > API Keys.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + ), + 'public_key' => array( + 'title' => __( 'Public key', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API keys from your Simplify account: Settings > API Keys.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + ), + 'private_key' => array( + 'title' => __( 'Private key', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API keys from your Simplify account: Settings > API Keys.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + ), + ); + } + + /** + * Payment form on checkout page. + */ + public function payment_fields() { + $description = $this->get_description(); + + if ( 'yes' == $this->sandbox ) { + $description .= ' ' . sprintf( __( 'TEST MODE ENABLED. Use a test card: %s', 'woocommerce' ), 'https://www.simplify.com/commerce/docs/tutorial/index#testing' ); + } + + if ( $description ) { + echo wpautop( wptexturize( trim( $description ) ) ); + } + + if ( 'standard' == $this->mode ) { + parent::payment_fields(); + } + } + + /** + * Outputs scripts used for simplify payment. + */ + public function payment_scripts() { + $load_scripts = false; + + if ( is_checkout() ) { + $load_scripts = true; + } + if ( $this->is_available() ) { + $load_scripts = true; + } + + if ( false === $load_scripts ) { + return; + } + + wp_enqueue_script( 'simplify-commerce', 'https://www.simplify.com/commerce/v1/simplify.js', array( 'jquery' ), WC_VERSION, true ); + wp_enqueue_script( 'wc-simplify-commerce', WC()->plugin_url() . '/includes/gateways/simplify-commerce/assets/js/simplify-commerce.js', array( 'simplify-commerce', 'wc-credit-card-form' ), WC_VERSION, true ); + wp_localize_script( 'wc-simplify-commerce', 'Simplify_commerce_params', array( + 'key' => $this->public_key, + 'card.number' => __( 'Card number', 'woocommerce' ), + 'card.expMonth' => __( 'Expiry month', 'woocommerce' ), + 'card.expYear' => __( 'Expiry year', 'woocommerce' ), + 'is_invalid' => __( 'is invalid', 'woocommerce' ), + 'mode' => $this->mode, + 'is_ssl' => is_ssl(), + ) ); + } + + public function add_payment_method() { + if ( empty( $_POST['simplify_token'] ) ) { + wc_add_notice( __( 'There was a problem adding this card.', 'woocommerce' ), 'error' ); + return; + } + + $cart_token = wc_clean( $_POST['simplify_token'] ); + $customer_token = $this->get_users_token(); + $current_user = wp_get_current_user(); + $customer_info = array( + 'email' => $current_user->user_email, + 'name' => $current_user->display_name, + ); + + $token = $this->save_token( $customer_token, $cart_token, $customer_info ); + if ( is_null( $token ) ) { + wc_add_notice( __( 'There was a problem adding this card.', 'woocommerce' ), 'error' ); + return; + } + + return array( + 'result' => 'success', + 'redirect' => wc_get_endpoint_url( 'payment-methods' ), + ); + } + + /** + * Actually saves a customer token to the database. + * + * @param WC_Payment_Token $customer_token Payment Token + * @param string $cart_token CC Token + * @param array $customer_info 'email', 'name' + * + * @return null|WC_Payment_Token|WC_Payment_Token_CC + */ + public function save_token( $customer_token, $cart_token, $customer_info ) { + if ( ! is_null( $customer_token ) ) { + $customer = Simplify_Customer::findCustomer( $customer_token->get_token() ); + $updates = array( 'token' => $cart_token ); + $customer->setAll( $updates ); + $customer->updateCustomer(); + $customer = Simplify_Customer::findCustomer( $customer_token->get_token() ); // get updated customer with new set card + $token = $customer_token; + } else { + $customer = Simplify_Customer::createCustomer( array( + 'token' => $cart_token, + 'email' => $customer_info['email'], + 'name' => $customer_info['name'], + ) ); + $token = new WC_Payment_Token_CC(); + $token->set_token( $customer->id ); + } + + // If we were able to create an save our card, save the data on our side too + if ( is_object( $customer ) && '' != $customer->id ) { + $customer_properties = $customer->getProperties(); + $card = $customer_properties['card']; + $token->set_gateway_id( $this->id ); + $token->set_card_type( strtolower( $card->type ) ); + $token->set_last4( $card->last4 ); + $expiry_month = ( 1 === strlen( $card->expMonth ) ? '0' . $card->expMonth : $card->expMonth ); + $token->set_expiry_month( $expiry_month ); + $token->set_expiry_year( '20' . $card->expYear ); + if ( is_user_logged_in() ) { + $token->set_user_id( get_current_user_id() ); + } + $token->save(); + return $token; + } + + return null; + } + + /** + * Process customer: updating or creating a new customer/saved CC + * + * @param WC_Order $order Order object + * @param WC_Payment_Token $customer_token Payment Token + * @param string $cart_token CC Token + */ + protected function process_customer( $order, $customer_token = null, $cart_token = '' ) { + // Are we saving a new payment method? + if ( is_user_logged_in() && isset( $_POST['wc-simplify_commerce-new-payment-method'] ) && true === (bool) $_POST['wc-simplify_commerce-new-payment-method'] ) { + $customer_info = array( + 'email' => $order->get_billing_email(), + 'name' => trim( $order->get_formatted_billing_full_name() ), + ); + $token = $this->save_token( $customer_token, $cart_token, $customer_info ); + if ( ! is_null( $token ) ) { + $order->add_payment_token( $token ); + } + } + } + + /** + * Process standard payments. + * + * @param WC_Order $order + * @param string $cart_token + * @param string $customer_token + * + * @return array + * @uses Simplify_ApiException + * @uses Simplify_BadRequestException + */ + protected function process_standard_payments( $order, $cart_token = '', $customer_token = '' ) { + try { + + if ( empty( $cart_token ) && empty( $customer_token ) ) { + $error_msg = __( 'Please make sure your card details have been entered correctly and that your browser supports JavaScript.', 'woocommerce' ); + + if ( 'yes' == $this->sandbox ) { + $error_msg .= ' ' . __( 'Developers: Please make sure that you are including jQuery and there are no JavaScript errors on the page.', 'woocommerce' ); + } + + throw new Simplify_ApiException( $error_msg ); + } + + // We need to figure out if we want to charge the card token (new unsaved token, no customer, etc) + // or the customer token (just saved method, previously saved method) + $pass_tokens = array(); + + if ( ! empty( $cart_token ) ) { + $pass_tokens['token'] = $cart_token; + } + + if ( ! empty( $customer_token ) ) { + $pass_tokens['customer'] = $customer_token; + // Use the customer token only, since we already saved the (one time use) card token to the customer + if ( isset( $_POST['wc-simplify_commerce-new-payment-method'] ) && true === (bool) $_POST['wc-simplify_commerce-new-payment-method'] ) { + unset( $pass_tokens['token'] ); + } + } + + // Did we create an account and save a payment method? We might need to use the customer token instead of the card token + if ( isset( $_POST['createaccount'] ) && true === (bool) $_POST['createaccount'] && empty( $customer_token ) ) { + $user_token = $this->get_users_token(); + if ( ! is_null( $user_token ) ) { + $pass_tokens['customer'] = $user_token->get_token(); + unset( $pass_tokens['token'] ); + } + } + + $payment_response = $this->do_payment( $order, $order->get_total(), $pass_tokens ); + + if ( is_wp_error( $payment_response ) ) { + throw new Simplify_ApiException( $payment_response->get_error_message() ); + } else { + // Remove cart + WC()->cart->empty_cart(); + + // Return thank you page redirect + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $order ), + ); + } + } catch ( Simplify_ApiException $e ) { + if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { + foreach ( $e->getFieldErrors() as $error ) { + wc_add_notice( $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')', 'error' ); + } + } else { + wc_add_notice( $e->getMessage(), 'error' ); + } + + return array( + 'result' => 'fail', + 'redirect' => '', + ); + } + } + + /** + * do payment function. + * + * @param WC_order $order + * @param int $amount (default: 0) + * @param array $token + * + * @return bool|WP_Error + * @uses Simplify_BadRequestException + */ + public function do_payment( $order, $amount = 0, $token = array() ) { + if ( $amount * 100 < 50 ) { + return new WP_Error( 'simplify_error', __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce' ) ); + } + + try { + // Charge the customer + $data = array( + 'amount' => $amount * 100, // In cents. + 'description' => sprintf( __( '%1$s - Order #%2$s', 'woocommerce' ), esc_html( get_bloginfo( 'name', 'display' ) ), $order->get_order_number() ), + 'currency' => strtoupper( get_woocommerce_currency() ), + 'reference' => $order->get_id(), + ); + + $data = array_merge( $data, $token ); + $payment = Simplify_Payment::createPayment( $data ); + + } catch ( Exception $e ) { + + $error_message = $e->getMessage(); + + if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { + $error_message = ''; + foreach ( $e->getFieldErrors() as $error ) { + $error_message .= ' ' . $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')'; + } + } + + $order->add_order_note( sprintf( __( 'Simplify payment error: %s.', 'woocommerce' ), $error_message ) ); + + return new WP_Error( 'simplify_payment_declined', $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + if ( 'APPROVED' == $payment->paymentStatus ) { + // Payment complete + $order->payment_complete( $payment->id ); + + // Add order note + $order->add_order_note( sprintf( __( 'Simplify payment approved (ID: %1$s, Auth Code: %2$s)', 'woocommerce' ), $payment->id, $payment->authCode ) ); + + return true; + } else { + $order->add_order_note( __( 'Simplify payment declined', 'woocommerce' ) ); + + return new WP_Error( 'simplify_payment_declined', __( 'Payment was declined - please try another card.', 'woocommerce' ) ); + } + } + + /** + * Process standard payments. + * + * @param WC_Order $order + * @return array + */ + protected function process_hosted_payments( $order ) { + return array( + 'result' => 'success', + 'redirect' => $order->get_checkout_payment_url( true ), + ); + } + + protected function get_users_token() { + $customer_token = null; + if ( is_user_logged_in() ) { + $tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() ); + foreach ( $tokens as $token ) { + if ( $token->get_gateway_id() === $this->id ) { + $customer_token = $token; + break; + } + } + } + return $customer_token; + } + + /** + * Process the payment. + * + * @param int $order_id + * + * @return array|void + */ + public function process_payment( $order_id ) { + $order = wc_get_order( $order_id ); + + // Payment/CC form is hosted on Simplify + if ( 'hosted' === $this->mode ) { + return $this->process_hosted_payments( $order ); + } + + // New CC info was entered + if ( isset( $_POST['simplify_token'] ) ) { + $cart_token = wc_clean( $_POST['simplify_token'] ); + $customer_token = $this->get_users_token(); + $customer_token_value = ( ! is_null( $customer_token ) ? $customer_token->get_token() : '' ); + $this->process_customer( $order, $customer_token, $cart_token ); + return $this->process_standard_payments( $order, $cart_token, $customer_token_value ); + } + + // Possibly Create (or update) customer/save payment token, use an existing token, and then process the payment + if ( isset( $_POST['wc-simplify_commerce-payment-token'] ) && 'new' !== $_POST['wc-simplify_commerce-payment-token'] ) { + $token_id = wc_clean( $_POST['wc-simplify_commerce-payment-token'] ); + $token = WC_Payment_Tokens::get( $token_id ); + if ( $token->get_user_id() !== get_current_user_id() ) { + wc_add_notice( __( 'Please make sure your card details have been entered correctly and that your browser supports JavaScript.', 'woocommerce' ), 'error' ); + return; + } + $this->process_customer( $order, $token ); + return $this->process_standard_payments( $order, '', $token->get_token() ); + } + } + + /** + * Hosted payment args. + * + * @param WC_Order $order + * + * @return array + */ + protected function get_hosted_payments_args( $order ) { + $args = apply_filters( 'woocommerce_simplify_commerce_hosted_args', array( + 'sc-key' => $this->public_key, + 'amount' => $order->get_total() * 100, + 'reference' => $order->get_id(), + 'name' => esc_html( get_bloginfo( 'name', 'display' ) ), + 'description' => sprintf( __( 'Order #%s', 'woocommerce' ), $order->get_order_number() ), + 'receipt' => 'false', + 'color' => $this->modal_color, + 'redirect-url' => WC()->api_request_url( 'WC_Gateway_Simplify_Commerce' ), + 'address' => $order->get_billing_address_1() . ' ' . $order->get_billing_address_2(), + 'address-city' => $order->get_billing_city(), + 'address-state' => $order->get_billing_state(), + 'address-zip' => $order->get_billing_postcode(), + 'address-country' => $order->get_billing_country(), + 'operation' => 'create.token', + ), $order->get_id() ); + + return $args; + } + + /** + * Receipt page. + * + * @param int $order_id + */ + public function receipt_page( $order_id ) { + $order = wc_get_order( $order_id ); + + echo '

    ' . __( 'Thank you for your order, please click the button below to pay with credit card using Simplify Commerce by MasterCard.', 'woocommerce' ) . '

    '; + + $args = $this->get_hosted_payments_args( $order ); + $button_args = array(); + foreach ( $args as $key => $value ) { + $button_args[] = 'data-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"'; + } + + echo ' + ' . __( 'Cancel order & restore cart', 'woocommerce' ) . ' + '; + } + + /** + * Return handler for Hosted Payments. + */ + public function return_handler() { + @ob_clean(); + header( 'HTTP/1.1 200 OK' ); + + if ( isset( $_REQUEST['reference'] ) && isset( $_REQUEST['paymentId'] ) && isset( $_REQUEST['signature'] ) ) { + $signature = strtoupper( md5( $_REQUEST['amount'] . $_REQUEST['reference'] . $_REQUEST['paymentId'] . $_REQUEST['paymentDate'] . $_REQUEST['paymentStatus'] . $this->private_key ) ); + $order_id = absint( $_REQUEST['reference'] ); + $order = wc_get_order( $order_id ); + + if ( hash_equals( $signature, $_REQUEST['signature'] ) ) { + $order_complete = $this->process_order_status( $order, $_REQUEST['paymentId'], $_REQUEST['paymentStatus'], $_REQUEST['paymentDate'] ); + + if ( ! $order_complete ) { + $order->update_status( 'failed', __( 'Payment was declined by Simplify Commerce.', 'woocommerce' ) ); + } + + wp_redirect( $this->get_return_url( $order ) ); + exit(); + } + } + + wp_redirect( wc_get_page_permalink( 'cart' ) ); + exit(); + } + + /** + * Process the order status. + * + * @param WC_Order $order + * @param string $payment_id + * @param string $status + * @param string $auth_code + * + * @return bool + */ + public function process_order_status( $order, $payment_id, $status, $auth_code ) { + if ( 'APPROVED' == $status ) { + // Payment complete + $order->payment_complete( $payment_id ); + + // Add order note + $order->add_order_note( sprintf( __( 'Simplify payment approved (ID: %1$s, Auth Code: %2$s)', 'woocommerce' ), $payment_id, $auth_code ) ); + + // Remove cart + WC()->cart->empty_cart(); + + return true; + } + + return false; + } + + /** + * Process refunds. + * WooCommerce 2.2 or later. + * + * @param int $order_id + * @param float $amount + * @param string $reason + * @uses Simplify_ApiException + * @uses Simplify_BadRequestException + * @return bool|WP_Error + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + try { + $payment_id = get_post_meta( $order_id, '_transaction_id', true ); + + $refund = Simplify_Refund::createRefund( array( + 'amount' => $amount * 100, // In cents. + 'payment' => $payment_id, + 'reason' => $reason, + 'reference' => $order_id, + ) ); + + if ( 'APPROVED' == $refund->paymentStatus ) { + return true; + } else { + throw new Simplify_ApiException( __( 'Refund was declined.', 'woocommerce' ) ); + } + } catch ( Simplify_ApiException $e ) { + if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { + foreach ( $e->getFieldErrors() as $error ) { + return new WP_Error( 'simplify_refund_error', $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')' ); + } + } else { + return new WP_Error( 'simplify_refund_error', $e->getMessage() ); + } + } + + return false; + } + + /** + * Get gateway icon. + * + * @access public + * @return string + */ + public function get_icon() { + $icon = 'Visa'; + $icon .= 'MasterCard'; + $icon .= 'Discover'; + $icon .= 'Amex'; + $icon .= 'JCB'; + + return apply_filters( 'woocommerce_gateway_icon', $icon, $this->id ); + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify.php b/includes/gateways/simplify-commerce/includes/Simplify.php new file mode 100644 index 00000000000..d0c4859f5c4 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify.php @@ -0,0 +1,88 @@ +setAll($hash); + } + + /** + * Creates an OAuth access token + * @param $code - the authorisation code returned from the GET on /oauth/authorize + * @param $redirect_uri = this must be the redirect_uri set in the apps configuration + * @param $authentication - Authentication information to access the API. If not value is passed the global key Simplify::$publicKey and Simplify::$privateKey are used + * @return Simplify_AccessToken + */ + public static function create($code, $redirect_uri, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 3); + + $props = 'code='.$code.'&redirect_uri='.$redirect_uri.'&grant_type=authorization_code'; + $resp = Simplify_AccessToken::sendRequest($props, "token", $authentication); + + return new Simplify_AccessToken($resp); + } + + /** + * Refreshes the current token. The access_token and refresh_token values will be updated. + * @param $authentication - Authentication information to access the API. If not value is passed the global key Simplify::$publicKey and Simplify::$privateKey are used + * @return Simplify_AccessToken + * @throws InvalidArgumentException + */ + public function refresh($authentication = null) { + $args = func_get_args(); + + $refresh_token = $this->refresh_token; + if (empty($refresh_token)){ + throw new InvalidArgumentException('Cannot refresh access token; refresh token is invalid'); + } + + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $props = 'refresh_token='.$refresh_token.'&grant_type=refresh_token'; + $resp = Simplify_AccessToken::sendRequest($props, "token", $authentication); + + $this->setAll($resp); + + return $this; + } + + /** + *

    Revokes a token from further use. + * @param $authentication - Authentication information to access the API. If not value is passed the global key Simplify::$publicKey and Simplify::$privateKey are used + * @return Simplify_AccessToken + * @throws InvalidArgumentException + */ + public function revoke($authentication = null) { + $args = func_get_args(); + + $access_token = $this->access_token; + if (empty($access_token)){ + throw new InvalidArgumentException('Cannot revoke access token; access token is invalid'); + } + + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $props = 'token='.$access_token.''; + + Simplify_AccessToken::sendRequest($props, "revoke", $authentication); + + $this->access_token = null; + $this->refresh_token = null; + $this->redirect_uri = null; + + return $this; + } + + private static function sendRequest($props, $context, $authentication){ + + $url = Simplify_Constants::OAUTH_BASE_URL.'/'.$context; + $http = new Simplify_HTTP(); + $resp = $http->oauthRequest($url, $props, $authentication); + + return $resp; + } + + /** + * @ignore + */ + static public function getClazz() { + return "AccessToken"; + } + +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Authentication.php b/includes/gateways/simplify-commerce/includes/Simplify/Authentication.php new file mode 100644 index 00000000000..c787b6c1d5f --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Authentication.php @@ -0,0 +1,74 @@ +accessToken = $accessToken; + } + + function __construct2($publicKey, $privateKey) { + $this->publicKey = $publicKey; + $this->privateKey = $privateKey; + } + + function __construct3($publicKey, $privateKey, $accessToken) { + $this->publicKey = $publicKey; + $this->privateKey = $privateKey; + $this->accessToken = $accessToken; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Authorization.php b/includes/gateways/simplify-commerce/includes/Simplify/Authorization.php new file mode 100644 index 00000000000..69704b558f7 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Authorization.php @@ -0,0 +1,142 @@ + + *

    amount
    Amount of the payment (in the smallest unit of your currency). Example: 100 = $1.00USD required
    + *
    card.addressCity
    City of the cardholder. [max length: 50, min length: 2]
    + *
    card.addressCountry
    Country code (ISO-3166-1-alpha-2 code) of residence of the cardholder. [max length: 2, min length: 2]
    + *
    card.addressLine1
    Address of the cardholder. [max length: 255]
    + *
    card.addressLine2
    Address of the cardholder if needed. [max length: 255]
    + *
    card.addressState
    State of residence of the cardholder. For the US, this is a 2-digit USPS code. [max length: 255, min length: 2]
    + *
    card.addressZip
    Postal code of the cardholder. The postal code size is between 5 and 9 characters in length and only contains numbers or letters. [max length: 9, min length: 3]
    + *
    card.cvc
    CVC security code of the card. This is the code on the back of the card. Example: 123
    + *
    card.expMonth
    Expiration month of the card. Format is MM. Example: January = 01 [min value: 1, max value: 12] required
    + *
    card.expYear
    Expiration year of the card. Format is YY. Example: 2013 = 13 [min value: 0, max value: 99] required
    + *
    card.name
    Name as it appears on the card. [max length: 50, min length: 2]
    + *
    card.number
    Card number as it appears on the card. [max length: 19, min length: 13] required
    + *
    currency
    Currency code (ISO-4217) for the transaction. Must match the currency associated with your account. [default: USD] required
    + *
    customer
    ID of customer. If specified, card on file of customer will be used.
    + *
    description
    Free form text field to be used as a description of the payment. This field is echoed back with the payment on any find or list operations. [max length: 1024]
    + *
    reference
    Custom reference field to be used with outside systems.
    + *
    replayId
    An identifier that can be sent to uniquely identify a payment request to facilitate retries due to I/O related issues. This identifier must be unique for your account (sandbox or live) across all of your payments. If supplied, we will check for a payment on your account that matches this identifier, and if one is found we will attempt to return an identical response of the original request. [max length: 50, min length: 1]
    + *
    statementDescription.name
    Merchant name required
    + *
    statementDescription.phoneNumber
    Merchant contact phone number.
    + *
    token
    If specified, card associated with card token will be used. [max length: 255]
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Authorization a Authorization object. + */ + static public function createAuthorization($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Authorization(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_Authorization object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteAuthorization($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_Authorization objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in pagination of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: dateCreated amount id description paymentDate.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Authorization objects and the total + * number of Authorization objects available for the given criteria. + * @see ResourceList + */ + static public function listAuthorization($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Authorization(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Authorization object from the API + * + * @param string id the id of the Authorization object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Authorization a Authorization object + */ + static public function findAuthorization($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Authorization(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + /** + * @ignore + */ + public function getClazz() { + return "Authorization"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/CardToken.php b/includes/gateways/simplify-commerce/includes/Simplify/CardToken.php new file mode 100644 index 00000000000..0cd0f0cf44a --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/CardToken.php @@ -0,0 +1,94 @@ + + *
    callback
    The URL callback for the cardtoken
    + *
    card.addressCity
    City of the cardholder. [max length: 50, min length: 2]
    + *
    card.addressCountry
    Country code (ISO-3166-1-alpha-2 code) of residence of the cardholder. [max length: 2, min length: 2]
    + *
    card.addressLine1
    Address of the cardholder. [max length: 255]
    + *
    card.addressLine2
    Address of the cardholder if needed. [max length: 255]
    + *
    card.addressState
    State of residence of the cardholder. For the US, this is a 2-digit USPS code. [max length: 255, min length: 2]
    + *
    card.addressZip
    Postal code of the cardholder. The postal code size is between 5 and 9 in length and only contain numbers or letters. [max length: 9, min length: 3]
    + *
    card.cvc
    CVC security code of the card. This is the code on the back of the card. Example: 123
    + *
    card.expMonth
    Expiration month of the card. Format is MM. Example: January = 01 [min value: 1, max value: 12] required
    + *
    card.expYear
    Expiration year of the card. Format is YY. Example: 2013 = 13 [min value: 0, max value: 99] required
    + *
    card.name
    Name as appears on the card. [max length: 50, min length: 2]
    + *
    card.number
    Card number as it appears on the card. [max length: 19, min length: 13] required
    + *
    key
    Key used to create the card token.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return CardToken a CardToken object. + */ + static public function createCardToken($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_CardToken(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + + /** + * Retrieve a Simplify_CardToken object from the API + * + * @param string id the id of the CardToken object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return CardToken a CardToken object + */ + static public function findCardToken($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_CardToken(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + /** + * @ignore + */ + public function getClazz() { + return "CardToken"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Chargeback.php b/includes/gateways/simplify-commerce/includes/Simplify/Chargeback.php new file mode 100644 index 00000000000..78fca9c80af --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Chargeback.php @@ -0,0 +1,86 @@ + + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: id amount description dateCreated.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Chargeback objects and the total + * number of Chargeback objects available for the given criteria. + * @see ResourceList + */ + static public function listChargeback($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Chargeback(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Chargeback object from the API + * + * @param string id the id of the Chargeback object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Chargeback a Chargeback object + */ + static public function findChargeback($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Chargeback(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + /** + * @ignore + */ + public function getClazz() { + return "Chargeback"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Constants.php b/includes/gateways/simplify-commerce/includes/Simplify/Constants.php new file mode 100644 index 00000000000..e96ac699cd6 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Constants.php @@ -0,0 +1,58 @@ + + *
    amountOff
    Amount off of the price of the product in the smallest units of the currency of the merchant. While this field is optional, you must provide either amountOff or percentOff for a coupon. Example: 100 = $1.00USD [min value: 1]
    + *
    couponCode
    Code that identifies the coupon to be used. [min length: 2] required
    + *
    description
    A brief section that describes the coupon.
    + *
    durationInMonths
    DEPRECATED - Duration in months that the coupon will be applied after it has first been selected. [min value: 1, max value: 9999]
    + *
    endDate
    Last date of the coupon in UTC millis that the coupon can be applied to a subscription. This ends at 23:59:59 of the merchant timezone.
    + *
    maxRedemptions
    Maximum number of redemptions allowed for the coupon. A redemption is defined as when the coupon is applied to the subscription for the first time. [min value: 1]
    + *
    numTimesApplied
    The number of times a coupon will be applied on a customer's subscription. [min value: 1, max value: 9999]
    + *
    percentOff
    Percentage off of the price of the product. While this field is optional, you must provide either amountOff or percentOff for a coupon. The percent off is a whole number. [min value: 1, max value: 100]
    + *
    startDate
    First date of the coupon in UTC millis that the coupon can be applied to a subscription. This starts at midnight of the merchant timezone. required
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Coupon a Coupon object. + */ + static public function createCoupon($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Coupon(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_Coupon object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteCoupon($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_Coupon objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: dateCreated maxRedemptions timesRedeemed id startDate endDate percentOff couponCode durationInMonths numTimesApplied amountOff.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Coupon objects and the total + * number of Coupon objects available for the given criteria. + * @see ResourceList + */ + static public function listCoupon($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Coupon(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Coupon object from the API + * + * @param string id the id of the Coupon object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Coupon a Coupon object + */ + static public function findCoupon($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Coupon(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_Coupon object. + * + * The properties that can be updated: + *
    + *
    endDate
    The ending date in UTC millis for the coupon. This must be after the starting date of the coupon.
    + *
    maxRedemptions
    Maximum number of redemptions allowed for the coupon. A redemption is defined as when the coupon is applied to the subscription for the first time. [min value: 1]
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Coupon a Coupon object. + */ + public function updateCoupon($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "Coupon"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Customer.php b/includes/gateways/simplify-commerce/includes/Simplify/Customer.php new file mode 100644 index 00000000000..6bbcf524290 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Customer.php @@ -0,0 +1,184 @@ + + *
    card.addressCity
    City of the cardholder. required
    + *
    card.addressCountry
    Country code (ISO-3166-1-alpha-2 code) of residence of the cardholder. required
    + *
    card.addressLine1
    Address of the cardholder required
    + *
    card.addressLine2
    Address of the cardholder if needed. required
    + *
    card.addressState
    State of residence of the cardholder. For the US, this is a 2-digit USPS code. required
    + *
    card.addressZip
    Postal code of the cardholder. The postal code size is between 5 and 9 in length and only contain numbers or letters. required
    + *
    card.cvc
    CVC security code of the card. This is the code on the back of the card. Example: 123 required
    + *
    card.expMonth
    Expiration month of the card. Format is MM. Example: January = 01 required
    + *
    card.expYear
    Expiration year of the card. Format is YY. Example: 2013 = 13 required
    + *
    card.id
    ID of card. Unused during customer create.
    + *
    card.name
    Name as appears on the card. required
    + *
    card.number
    Card number as it appears on the card. [max length: 19, min length: 13]
    + *
    email
    Email address of the customer required
    + *
    name
    Customer name [min length: 2] required
    + *
    reference
    Reference field for external applications use.
    + *
    subscriptions.amount
    Amount of payment in the smallest unit of your currency. Example: 100 = $1.00USD
    + *
    subscriptions.billingCycle
    How the plan is billed to the customer. Values must be AUTO (indefinitely until the customer cancels) or FIXED (a fixed number of billing cycles). [default: AUTO]
    + *
    subscriptions.billingCycleLimit
    The number of fixed billing cycles for a plan. Only used if the billingCycle parameter is set to FIXED. Example: 4
    + *
    subscriptions.coupon
    Coupon associated with the subscription for the customer.
    + *
    subscriptions.currency
    Currency code (ISO-4217). Must match the currency associated with your account. [default: USD]
    + *
    subscriptions.customer
    The customer ID to create the subscription for. Do not supply this when creating a customer.
    + *
    subscriptions.frequency
    Frequency of payment for the plan. Used in conjunction with frequencyPeriod. Valid values are "DAILY", "WEEKLY", "MONTHLY" and "YEARLY".
    + *
    subscriptions.frequencyPeriod
    Period of frequency of payment for the plan. Example: if the frequency is weekly, and periodFrequency is 2, then the subscription is billed bi-weekly.
    + *
    subscriptions.name
    Name describing subscription
    + *
    subscriptions.plan
    The plan ID that the subscription should be created from.
    + *
    subscriptions.quantity
    Quantity of the plan for the subscription. [min value: 1]
    + *
    subscriptions.renewalReminderLeadDays
    If set, how many days before the next billing cycle that a renewal reminder is sent to the customer. If null, then no emails are sent. Minimum value is 7 if set.
    + *
    token
    If specified, card associated with card token will be used
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Customer a Customer object. + */ + static public function createCustomer($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Customer(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_Customer object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteCustomer($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_Customer objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: dateCreated id name email reference.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Customer objects and the total + * number of Customer objects available for the given criteria. + * @see ResourceList + */ + static public function listCustomer($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Customer(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Customer object from the API + * + * @param string id the id of the Customer object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Customer a Customer object + */ + static public function findCustomer($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Customer(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_Customer object. + * + * The properties that can be updated: + *
    + *
    card.addressCity
    City of the cardholder. required
    + *
    card.addressCountry
    Country code (ISO-3166-1-alpha-2 code) of residence of the cardholder. required
    + *
    card.addressLine1
    Address of the cardholder. required
    + *
    card.addressLine2
    Address of the cardholder if needed. required
    + *
    card.addressState
    State of residence of the cardholder. For the US, this is a 2-digit USPS code. required
    + *
    card.addressZip
    Postal code of the cardholder. The postal code size is between 5 and 9 in length and only contain numbers or letters. required
    + *
    card.cvc
    CVC security code of the card. This is the code on the back of the card. Example: 123 required
    + *
    card.expMonth
    Expiration month of the card. Format is MM. Example: January = 01 required
    + *
    card.expYear
    Expiration year of the card. Format is YY. Example: 2013 = 13 required
    + *
    card.id
    ID of card. If present, card details for the customer will not be updated. If not present, the customer will be updated with the supplied card details.
    + *
    card.name
    Name as appears on the card. required
    + *
    card.number
    Card number as it appears on the card. [max length: 19, min length: 13]
    + *
    email
    Email address of the customer required
    + *
    name
    Customer name [min length: 2] required
    + *
    reference
    Reference field for external applications use.
    + *
    token
    If specified, card associated with card token will be added to the customer
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Customer a Customer object. + */ + public function updateCustomer($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "Customer"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Deposit.php b/includes/gateways/simplify-commerce/includes/Simplify/Deposit.php new file mode 100644 index 00000000000..7c6d66add3f --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Deposit.php @@ -0,0 +1,86 @@ + + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: amount dateCreated depositDate.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Deposit objects and the total + * number of Deposit objects available for the given criteria. + * @see ResourceList + */ + static public function listDeposit($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Deposit(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Deposit object from the API + * + * @param string id the id of the Deposit object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Deposit a Deposit object + */ + static public function findDeposit($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Deposit(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + /** + * @ignore + */ + public function getClazz() { + return "Deposit"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Event.php b/includes/gateways/simplify-commerce/includes/Simplify/Event.php new file mode 100644 index 00000000000..b632c9dcaad --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Event.php @@ -0,0 +1,68 @@ +paylod
    The raw JWS payload.
    required + *
    url
    The URL for the webhook. If present it must match the URL registered for the webhook.
    + * @param $authentication Object that contains the API public and private keys. If null the values of the static + * Simplify::$publicKey and Simplify::$privateKey will be used. + * @return Payments_Event an Event object. + * @throws InvalidArgumentException + */ + static public function createEvent($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $paymentsApi = new Simplify_PaymentsApi(); + + $jsonObject = $paymentsApi->jwsDecode($hash, $authentication); + + if ($jsonObject['event'] == null) { + throw new InvalidArgumentException("Incorect data in webhook event"); + } + + return $paymentsApi->convertFromHashToObject($jsonObject['event'], self::getClazz()); + } + + /** + * @ignore + */ + static public function getClazz() { + return "Event"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Exceptions.php b/includes/gateways/simplify-commerce/includes/Simplify/Exceptions.php new file mode 100644 index 00000000000..2e890ebd254 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Exceptions.php @@ -0,0 +1,294 @@ +status = $status; + $this->errorCode = null; + $this->reference = null; + + if ($errorData != null) { + + $this->reference = $errorData['reference']; + $this->errorData = $errorData; + + $error = $errorData['error']; + if ($error != null) { + + $m = $error['message']; + if ($m != null) { + $this->message = $m; + } + + $this->errorCode = $error['code']; + } + } + } + + /** + * Returns a map of all error data returned by the API. + * @return array a map containing API error data. + */ + function getErrorData() { + return $this->errorData; + } + + /** + * Returns the HTTP status for the request. + * @return string HTTP status code (or null if there is no status). + */ + function getStatus() { + return $this->status; + } + + /** + * Returns unique reference for the API error. + * @return string a reference (or null if there is no reference). + */ + function getReference() { + return $this->reference; + } + + /** + * Returns an code for the API error. + * @return string the error code. + */ + function getErrorCode() { + return $this->errorCode; + } + + /** + * Returns a description of the error. + * @return string Description of the error. + */ + function describe() { + return get_class($this) . ": \"" + . $this->getMessage() . "\" (status: " + . $this->getStatus() . ", error code: " + . $this->getErrorCode() . ", reference: " + . $this->getReference() . ")"; + } + +} + + +/** + * Exception raised when there are communication problems contacting the API. + */ +class Simplify_ApiConnectionException extends Simplify_ApiException { + + /** + * @ignore + */ + function __construct($message, $status = null, $errorData = null) { + parent::__construct($message, $status, $errorData); + } +} + +/** + * Exception raised where there are problems authenticating a request. + */ +class Simplify_AuthenticationException extends Simplify_ApiException { + + /** + * @ignore + */ + function __construct($message, $status = null, $errorData = null) { + parent::__construct($message, $status, $errorData); + } +} + +/** + * Exception raised when the API request contains errors. + */ +class Simplify_BadRequestException extends Simplify_ApiException { + + protected $fieldErrors; + + /** + * @ignore + */ + function __construct($message, $status = null, $errorData = null) { + parent::__construct($message, $status, $errorData); + + $fieldErrors = array(); + + if ($errorData != null) { + $error = $errorData['error']; + if ($error != null) { + $fieldErrors = $error['fieldErrors']; + if ($fieldErrors != null) { + $this->fieldErrors = array(); + foreach ($fieldErrors as $fieldError) { + array_push($this->fieldErrors, new Simplify_FieldError($fieldError)); + } + } + } + } + } + + /** + * Returns a boolean indicating whether there are any field errors. + * @return boolean true if there are field errors; false otherwise. + */ + function hasFieldErrors() { + return count($this->fieldErrors) > 0; + } + + /** + * Returns a list containing all field errors. + * @return array list of field errors. + */ + function getFieldErrors() { + return $this->fieldErrors; + } + + /** + * Returns a description of the error. + * @return string description of the error. + */ + function describe() { + $s = parent::describe(); + foreach ($this->getFieldErrors() as $fieldError) { + $s = $s . "\n" . (string) $fieldError; + } + return $s . "\n"; + } + +} + +/** + * Represents a single error in a field of a request sent to the API. + */ +class Simplify_FieldError { + + protected $field; + protected $code; + protected $message; + + /** + * @ignore + */ + function __construct($errorData) { + + $this->field = $errorData['field']; + $this->code = $errorData['code']; + $this->message = $errorData['message']; + } + + /** + * Returns the name of the field with the error. + * @return string the field name. + */ + function getFieldName() { + return $this->field; + } + + /** + * Returns the code for the error. + * @return string the error code. + */ + function getErrorCode() { + return $this->code; + } + + /** + * Returns a description of the error. + * @return string description of the error. + */ + function getMessage() { + return $this->message; + } + + + function __toString() { + return "Field error: " . $this->getFieldName() . "\"" . $this->getMessage() . "\" (" . $this->getErrorCode() . ")"; + } + +} + +/** + * Exception when a requested object cannot be found. + */ +class Simplify_ObjectNotFoundException extends Simplify_ApiException { + + /** + * @ignore + */ + function __construct($message, $status = null, $errorData = null) { + parent::__construct($message, $status, $errorData); + } +} + +/** + * Exception when a request was not allowed. + */ +class Simplify_NotAllowedException extends Simplify_ApiException { + + /** + * @ignore + */ + function __construct($message, $status = null, $errorData = null) { + parent::__construct($message, $status, $errorData); + } +} + +/** + * Exception when there was a system error processing a request. + */ +class Simplify_SystemException extends Simplify_ApiException { + + /** + * @ignore + */ + function __construct($message, $status = null, $errorData = null) { + parent::__construct($message, $status, $errorData); + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/FraudCheck.php b/includes/gateways/simplify-commerce/includes/Simplify/FraudCheck.php new file mode 100644 index 00000000000..d0fea2a5799 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/FraudCheck.php @@ -0,0 +1,122 @@ + + *
    amount
    Amount of the transaction to be checked for fraud (in the smallest unit of your currency). Example: 100 = $1.00USD
    + *
    card.addressCity
    City of the cardholder. [max length: 50, min length: 2]
    + *
    card.addressCountry
    Country code (ISO-3166-1-alpha-2 code) of residence of the cardholder. [max length: 2, min length: 2]
    + *
    card.addressLine1
    Address of the cardholder. [max length: 255]
    + *
    card.addressLine2
    Address of the cardholder if needed. [max length: 255]
    + *
    card.addressState
    State of residence of the cardholder. For the US, this is a 2-digit USPS code. [max length: 255, min length: 2]
    + *
    card.addressZip
    Postal code of the cardholder. The postal code size is between 5 and 9 characters in length and only contains numbers or letters. [max length: 9, min length: 3]
    + *
    card.cvc
    CVC security code of the card. This is the code on the back of the card. Example: 123
    + *
    card.expMonth
    Expiration month of the card. Format is MM. Example: January = 01 [min value: 1, max value: 12] required
    + *
    card.expYear
    Expiration year of the card. Format is YY. Example: 2013 = 13 [min value: 0, max value: 99] required
    + *
    card.name
    Name as it appears on the card. [max length: 50, min length: 2]
    + *
    card.number
    Card number as it appears on the card. [max length: 19, min length: 13] required
    + *
    currency
    Currency code (ISO-4217) for the transaction to be checked for fraud.
    + *
    description
    - Description of the fraud check.
    + *
    mode
    Fraud check mode. “simple” only does an AVS and CVC check; “advanced” does a complete fraud check, running the input against the set up rules. [valid values: simple, advanced, full] required
    + *
    sessionId
    Session ID usd during data collection. [max length: 255]
    + *
    token
    Description
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return FraudCheck a FraudCheck object. + */ + static public function createFraudCheck($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_FraudCheck(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + + /** + * Retrieve Simplify_FraudCheck objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Allows for ascending or descending sorting of the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Used in paging of the list. This is the start offset of the page. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: .
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of FraudCheck objects and the total + * number of FraudCheck objects available for the given criteria. + * @see ResourceList + */ + static public function listFraudCheck($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_FraudCheck(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_FraudCheck object from the API + * + * @param string id the id of the FraudCheck object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return FraudCheck a FraudCheck object + */ + static public function findFraudCheck($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_FraudCheck(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + /** + * @ignore + */ + public function getClazz() { + return "FraudCheck"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Http.php b/includes/gateways/simplify-commerce/includes/Simplify/Http.php new file mode 100644 index 00000000000..603038b6644 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Http.php @@ -0,0 +1,411 @@ + self::POST, + "put" => self::PUT, + "get" => self::GET, + "delete" => self::DELETE); + + private function request($url, $method, $authentication, $payload = '') + { + if ($authentication->publicKey == null) { + throw new InvalidArgumentException('Must have a valid public key to connect to the API'); + } + + if ($authentication->privateKey == null) { + throw new InvalidArgumentException('Must have a valid API key to connect to the API'); + } + + if (!array_key_exists(strtolower($method), self::$_validMethods)) { + throw new InvalidArgumentException('Invalid method: '.strtolower($method)); + } + + $method = self::$_validMethods[strtolower($method)]; + + $curl = curl_init(); + + $options = array(); + + $options[CURLOPT_URL] = $url; + $options[CURLOPT_CUSTOMREQUEST] = $method; + $options[CURLOPT_RETURNTRANSFER] = true; + $options[CURLOPT_FAILONERROR] = false; + + $signature = $this->jwsEncode($authentication, $url, $payload, $method == self::POST || $method == self::PUT); + + if ($method == self::POST || $method == self::PUT) { + $headers = array( + 'Content-type: application/json' + ); + $options[CURLOPT_POSTFIELDS] = $signature; + } else { + $headers = array( + 'Authorization: JWS ' . $signature + ); + } + + array_push($headers, 'Accept: application/json'); + $user_agent = 'PHP-SDK/' . Simplify_Constants::VERSION; + if (Simplify::$userAgent != null) { + $user_agent = $user_agent . ' ' . Simplify::$userAgent; + } + array_push($headers, 'User-Agent: ' . $user_agent); + + $options[CURLOPT_HTTPHEADER] = $headers; + + curl_setopt_array($curl, $options); + + $data = curl_exec($curl); + $errno = curl_errno($curl); + $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($data == false || $errno != CURLE_OK) { + throw new Simplify_ApiConnectionException(curl_error($curl)); + } + + $object = json_decode($data, true); + //'typ' => self::JWS_TYPE, + $response = array('status' => $status, 'object' => $object); + + return $response; + curl_close($curl); + } + + /** + * Handles Simplify API requests + * + * @param $url + * @param $method + * @param $authentication + * @param string $payload + * @return mixed + * @throws Simplify_AuthenticationException + * @throws Simplify_ObjectNotFoundException + * @throws Simplify_BadRequestException + * @throws Simplify_NotAllowedException + * @throws Simplify_SystemException + */ + public function apiRequest($url, $method, $authentication, $payload = ''){ + + $response = $this->request($url, $method, $authentication, $payload); + + $status = $response['status']; + $object = $response['object']; + + if ($status == self::HTTP_SUCCESS) { + return $object; + } + + if ($status == self::HTTP_REDIRECTED) { + throw new Simplify_BadRequestException("Unexpected response code returned from the API, have you got the correct URL?", $status, $object); + } else if ($status == self::HTTP_BAD_REQUEST) { + throw new Simplify_BadRequestException("Bad request", $status, $object); + } else if ($status == self::HTTP_UNAUTHORIZED) { + throw new Simplify_AuthenticationException("You are not authorized to make this request. Are you using the correct API keys?", $status, $object); + } else if ($status == self::HTTP_NOT_FOUND) { + throw new Simplify_ObjectNotFoundException("Object not found", $status, $object); + } else if ($status == self::HTTP_NOT_ALLOWED) { + throw new Simplify_NotAllowedException("Operation not allowed", $status, $object); + } else if ($status < 500) { + throw new Simplify_BadRequestException("Bad request", $status, $object); + } + throw new Simplify_SystemException("An unexpected error has been raised. Looks like there's something wrong at our end." , $status, $object); + } + + /** + * Handles Simplify OAuth requests + * + * @param $url + * @param $payload + * @param $authentication + * @return mixed + * @throws Simplify_AuthenticationException + * @throws Simplify_ObjectNotFoundException + * @throws Simplify_BadRequestException + * @throws Simplify_NotAllowedException + * @throws Simplify_SystemException + */ + public function oauthRequest($url, $payload, $authentication){ + + $response = $this->request($url, Simplify_HTTP::POST, $authentication, $payload); + + $status = $response['status']; + $object = $response['object']; + + if ($status == self::HTTP_SUCCESS) { + return $object; + } + + $error = $object['error']; + $error_description = $object['error_description']; + + if ($status == self::HTTP_REDIRECTED) { + throw new Simplify_BadRequestException("Unexpected response code returned from the API, have you got the correct URL?", $status, $object); + } else if ($status == self::HTTP_BAD_REQUEST) { + + if ( $error == 'invalid_request'){ + throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Error during OAuth request', $error, $error_description)); + }else if ($error == 'unsupported_grant_type'){ + throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Unsupported grant type in OAuth request', $error, $error_description)); + }else if ($error == 'invalid_scope'){ + throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Invalid scope in OAuth request', $error, $error_description)); + }else{ + throw new Simplify_BadRequestException("", $status, $this->buildOauthError('Unknown OAuth error', $error, $error_description)); + } + + //TODO: build BadRequestException error JSON + + } else if ($status == self::HTTP_UNAUTHORIZED){ + + if ($error == 'access_denied'){ + throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Access denied for OAuth request', $error, $error_description)); + }else if ($error == 'invalid_client'){ + throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Invalid client ID in OAuth request', $error, $error_description)); + }else if ($error == 'unauthorized_client'){ + throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Unauthorized client in OAuth request', $error, $error_description)); + }else{ + throw new Simplify_AuthenticationException("", $status, $this->buildOauthError('Unknown authentication error', $error, $error_description)); + } + + } else if ($status < 500) { + throw new Simplify_BadRequestException("Bad request", $status, $object); + } + throw new Simplify_SystemException("An unexpected error has been raised. Looks like there's something wrong at our end." , $status, $object); + } + + public function jwsDecode($authentication, $hash) + { + if ($authentication->publicKey == null) { + throw new InvalidArgumentException('Must have a valid public key to connect to the API'); + } + + if ($authentication->privateKey == null) { + throw new InvalidArgumentException('Must have a valid API key to connect to the API'); + } + + if (!isset($hash['payload'])) { + throw new InvalidArgumentException('Event data is Missing payload'); + } + $payload = trim($hash['payload']); + + + try { + $parts = explode('.', $payload); + if (count($parts) != 3) { + $this->jwsAuthError("Incorrectly formatted JWS message"); + } + + $headerStr = $this->jwsUrlSafeDecode64($parts[0]); + $bodyStr = $this->jwsUrlSafeDecode64($parts[1]); + $sigStr = $parts[2]; + + $url = null; + if (isset($hash['url'])) { + $url = $hash['url']; + } + $this->jwsVerifyHeader($headerStr, $url, $authentication->publicKey); + + $msg = $parts[0] . "." . $parts[1]; + if (!$this->jwsVerifySignature($authentication->privateKey, $msg, $sigStr)) { + $this->jwsAuthError("JWS signature does not match"); + } + + return $bodyStr; + + } catch (ApiException $e) { + throw $e; + } catch (Exception $e) { + $this->jwsAuthError("Exception during JWS decoding: " . $e); + } + } + + private function jwsEncode($authentication, $url, $payload, $hasPayload) + { + // TODO - better seeding of RNG + $jws_hdr = array('typ' => self::JWS_TYPE, + 'alg' => self::JWS_ALGORITHM, + 'kid' => $authentication->publicKey, + self::JWS_HDR_URI => $url, + self::JWS_HDR_TIMESTAMP => sprintf("%u000", round(microtime(true))), + self::JWS_HDR_NONCE => sprintf("%u", mt_rand()), + ); + + // add oauth token if provided + if ( !empty($authentication->accessToken) ){ + $jws_hdr[self::JWS_HDR_TOKEN] = $authentication->accessToken; + } + + $header = $this->jwsUrlSafeEncode64(json_encode($jws_hdr)); + + if ($hasPayload) { + $payload = $this->jwsUrlSafeEncode64($payload); + } else { + $payload = ''; + } + + $msg = $header . "." . $payload; + return $msg . "." . $this->jwsSign($authentication->privateKey, $msg); + } + + private function jwsSign($privateKey, $msg) { + $decodedPrivateKey = $this->jwsUrlSafeDecode64($privateKey); + $sig = hash_hmac('sha256', $msg, $decodedPrivateKey, true); + + return $this->jwsUrlSafeEncode64($sig); + } + + private function jwsVerifyHeader($header, $url, $publicKey) { + + $hdr = json_decode($header, true); + + if (count($hdr) != self::JWS_NUM_HEADERS) { + $this->jwsAuthError("Incorrect number of JWS header parameters - found " . count($hdr) . " required " . self::JWS_NUM_HEADERS); + } + + if ($hdr['alg'] != self::JWS_ALGORITHM) { + $this->jwsAuthError("Incorrect algorithm - found " . $hdr['alg'] . " required " . self::WS_ALGORITHM); + } + + if ($hdr['typ'] != self::JWS_TYPE) { + $this->jwsAuthError("Incorrect type - found " . $hdr['typ'] . " required " . self::JWS_TYPE); + } + + if ($hdr['kid'] == null) { + $this->jwsAuthError("Missing Key ID"); + } + + if ($hdr['kid'] != $publicKey) { + if ($this->isLiveKey($publicKey)) { + $this->jwsAuthError("Invalid Key ID"); + } + } + + if ($hdr[self::JWS_HDR_URI] == null) { + $this->jwsAuthError("Missing URI"); + } + + if ($url != null && $hdr[self::JWS_HDR_URI] != $url) { + $this->jwsAuthError("Incorrect URL - found " . $hdr[self::JWS_HDR_URI] . " required " . $url); + } + + + if ($hdr[self::JWS_HDR_TIMESTAMP] == null) { + $this->jwsAuthError("Missing timestamp"); + } + + if (!$this->jwsVerifyTimestamp($hdr[self::JWS_HDR_TIMESTAMP])) { + $this->jwsAuthError("Invalid timestamp"); + } + + if ($hdr[self::JWS_HDR_NONCE] == null) { + $this->jwsAuthError("Missing nonce"); + } + + if ($hdr[self::JWS_HDR_UNAME] == null) { + $this->jwsAuthError("Missing username"); + } + } + + + private function jwsVerifySignature($privateKey, $msg, $expectedSig) { + return $this->jwsSign($privateKey, $msg) == $expectedSig; + } + + private function jwsAuthError($reason) { + throw new Simplify_AuthenticationException("JWS authentication failure: " . $reason); + } + + private function jwsVerifyTimestamp($ts) { + $now = round(microtime(true)); // Seconds + return abs($now - $ts / 1000) < self::JWS_MAX_TIMESTAMP_DIFF; + } + + private function isLiveKey($k) { + return strpos($k, "lvpb") === 0; + } + + private function jwsUrlSafeEncode64($s) { + return str_replace(array('+', '/', '='), + array('-', '_', ''), + base64_encode($s)); + } + + private function jwsUrlSafeDecode64($s) { + + switch (strlen($s) % 4) { + case 0: break; + case 2: $s = $s . "=="; + break; + case 3: $s = $s . "="; + break; + default: throw new InvalidArgumentException('incorrecly formatted JWS payload'); + } + return base64_decode(str_replace(array('-', '_'), array('+', '/'), $s)); + } + + private function buildOauthError($msg, $error, $error_description){ + + return array( + 'error' => array( + 'code' => 'oauth_error', + 'message' => $msg.', error code: '.$error.', description: '.$error_description.'' + ) + ); + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Invoice.php b/includes/gateways/simplify-commerce/includes/Simplify/Invoice.php new file mode 100644 index 00000000000..afdbcc59172 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Invoice.php @@ -0,0 +1,228 @@ + + *
    billingAddress.city
    Billing address city of the location where the goods or services were supplied. [max length: 255, min length: 2]
    + *
    billingAddress.country
    Billing address country of the location where the goods or services were supplied. [max length: 2, min length: 2]
    + *
    billingAddress.line1
    Billing address line 1 of the location where the goods or services were supplied. [max length: 255]
    + *
    billingAddress.line2
    Billing address line 2 of the location where the goods or services were supplied. [max length: 255]
    + *
    billingAddress.name
    Billing address name of the location where the goods or services were supplied. Will use the customer name if not provided. [max length: 255]
    + *
    billingAddress.state
    Billing address state of the location where the goods or services were supplied. [max length: 255]
    + *
    billingAddress.zip
    Billing address zip of the location where the goods or services were supplied. [max length: 32]
    + *
    businessAddress.city
    Address city of the business that is sending the invoice. [max length: 255, min length: 2]
    + *
    businessAddress.country
    Address country of the business that is sending the invoice. [max length: 2, min length: 2]
    + *
    businessAddress.line1
    Address line 1 of the business that is sending the invoice. [max length: 255]
    + *
    businessAddress.line2
    Address line 2 of the business that is sending the invoice. [max length: 255]
    + *
    businessAddress.name
    The name of the business that is sending the invoice. [max length: 255]
    + *
    businessAddress.state
    Address state of the business that is sending the invoice. [max length: 255]
    + *
    businessAddress.zip
    Address zip of the business that is sending the invoice. [max length: 32]
    + *
    currency
    Currency code (ISO-4217). Must match the currency associated with your account. [max length: 3, min length: 3, default: USD]
    + *
    customer
    The customer ID of the customer we are invoicing. This is optional if invoiceToCopy or a name and email are provided
    + *
    customerTaxNo
    The tax number or VAT id of the person to whom the goods or services were supplied. [max length: 255]
    + *
    discountRate
    The discount percent as a decimal e.g. 12.5. This is used to calculate the discount amount which is subtracted from the total amount due before any tax is applied. [max length: 6]
    + *
    dueDate
    The date invoice payment is due. If a late fee is provided this will be added to the invoice total is the due date has past.
    + *
    email
    The email of the customer we are invoicing. This is optional if customer or invoiceToCopy is provided. A new customer will be created using the the name and email.
    + *
    invoiceId
    User defined invoice id. If not provided the system will generate a numeric id. [max length: 255]
    + *
    invoiceToCopy
    The id of an existing invoice to be copied. This is optional if customer or a name and email are provided
    + *
    items.amount
    Amount of the invoice item (the smallest unit of your currency). Example: 100 = $1.00USD [min value: -9999900, max value: 9999900] required
    + *
    items.description
    The description of the invoice item. [max length: 1024]
    + *
    items.invoice
    The ID of the invoice this item belongs to.
    + *
    items.product
    The product this invoice item refers to.
    + *
    items.quantity
    Quantity of the item. This total amount of the invoice item is the amount * quantity. [min value: 1, max value: 999999, default: 1]
    + *
    items.reference
    User defined reference field. [max length: 255]
    + *
    items.tax
    The tax ID of the tax charge in the invoice item.
    + *
    lateFee
    The late fee amount that will be added to the invoice total is the due date is past due. Value provided must be in the smallest unit of your currency. Example: 100 = $1.00USD [max value: 9999900]
    + *
    memo
    A memo that is displayed to the customer on the invoice payment screen. [max length: 4000]
    + *
    name
    The name of the customer we are invoicing. This is optional if customer or invoiceToCopy is provided. A new customer will be created using the the name and email. [max length: 50, min length: 2]
    + *
    note
    This field can be used to store a note that is not displayed to the customer. [max length: 4000]
    + *
    reference
    User defined reference field. [max length: 255]
    + *
    shippingAddress.city
    Address city of the location where the goods or services were supplied. [max length: 255, min length: 2]
    + *
    shippingAddress.country
    Address country of the location where the goods or services were supplied. [max length: 2, min length: 2]
    + *
    shippingAddress.line1
    Address line 1 of the location where the goods or services were supplied. [max length: 255]
    + *
    shippingAddress.line2
    Address line 2 of the location where the goods or services were supplied. [max length: 255]
    + *
    shippingAddress.name
    Address name of the location where the goods or services were supplied. [max length: 255]
    + *
    shippingAddress.state
    Address state of the location where the goods or services were supplied. [max length: 255]
    + *
    shippingAddress.zip
    Address zip of the location where the goods or services were supplied. [max length: 32]
    + *
    suppliedDate
    The date on which the goods or services were supplied.
    + *
    taxNo
    The tax number or VAT id of the person who supplied the goods or services. [max length: 255]
    + *
    type
    The type of invoice. One of WEB or MOBILE. [valid values: WEB, MOBILE, default: WEB]
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Invoice a Invoice object. + */ + static public function createInvoice($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Invoice(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_Invoice object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteInvoice($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_Invoice objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: id invoiceDate dueDate datePaid customer status dateCreated.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Invoice objects and the total + * number of Invoice objects available for the given criteria. + * @see ResourceList + */ + static public function listInvoice($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Invoice(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Invoice object from the API + * + * @param string id the id of the Invoice object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Invoice a Invoice object + */ + static public function findInvoice($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Invoice(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_Invoice object. + * + * The properties that can be updated: + *
    + *
    billingAddress.city
    Billing address city of the location where the goods or services were supplied. [max length: 255, min length: 2]
    + *
    billingAddress.country
    Billing address country of the location where the goods or services were supplied. [max length: 2, min length: 2]
    + *
    billingAddress.line1
    Billing address line 1 of the location where the goods or services were supplied. [max length: 255]
    + *
    billingAddress.line2
    Billing address line 2 of the location where the goods or services were supplied. [max length: 255]
    + *
    billingAddress.name
    Billing address name of the location where the goods or services were supplied. [max length: 255]
    + *
    billingAddress.state
    Billing address state of the location where the goods or services were supplied. [max length: 255]
    + *
    billingAddress.zip
    Billing address zip of the location where the goods or services were supplied. [max length: 32]
    + *
    businessAddress.city
    Business address city of the business that is sending the invoice. [max length: 255, min length: 2]
    + *
    businessAddress.country
    Business address country of the business that is sending the invoice. [max length: 2, min length: 2]
    + *
    businessAddress.line1
    Business address line 1 of the business that is sending the invoice. [max length: 255]
    + *
    businessAddress.line2
    Business address line 2 of the business that is sending the invoice. [max length: 255]
    + *
    businessAddress.name
    Business address name of the business that is sending the invoice. [max length: 255]
    + *
    businessAddress.state
    Business address state of the business that is sending the invoice. [max length: 255]
    + *
    businessAddress.zip
    Business address zip of the business that is sending the invoice. [max length: 32]
    + *
    currency
    Currency code (ISO-4217). Must match the currency associated with your account. [max length: 3, min length: 3]
    + *
    customerTaxNo
    The tax number or VAT id of the person to whom the goods or services were supplied. [max length: 255]
    + *
    datePaid
    This is the date the invoice was PAID in UTC millis.
    + *
    discountRate
    The discount percent as a decimal e.g. 12.5. This is used to calculate the discount amount which is subtracted from the total amount due before any tax is applied. [max length: 6]
    + *
    dueDate
    The date invoice payment is due. If a late fee is provided this will be added to the invoice total is the due date has past.
    + *
    email
    The email of the customer we are invoicing. This is optional if customer or invoiceToCopy is provided. A new customer will be created using the the name and email.
    + *
    invoiceId
    User defined invoice id. If not provided the system will generate a numeric id. [max length: 255]
    + *
    items.amount
    Amount of the invoice item in the smallest unit of your currency. Example: 100 = $1.00USD [min value: -9999900, max value: 9999900] required
    + *
    items.description
    The description of the invoice item. [max length: 1024]
    + *
    items.invoice
    The ID of the invoice this item belongs to.
    + *
    items.product
    The Id of the product this item refers to.
    + *
    items.quantity
    Quantity of the item. This total amount of the invoice item is the amount * quantity. [min value: 1, max value: 999999, default: 1]
    + *
    items.reference
    User defined reference field. [max length: 255]
    + *
    items.tax
    The tax ID of the tax charge in the invoice item.
    + *
    lateFee
    The late fee amount that will be added to the invoice total is the due date is past due. Value provided must be in the smallest unit of your currency. Example: 100 = $1.00USD [max value: 9999900]
    + *
    memo
    A memo that is displayed to the customer on the invoice payment screen. [max length: 4000]
    + *
    name
    The name of the customer we are invoicing. This is optional if customer or invoiceToCopy is provided. A new customer will be created using the the name and email. [max length: 50, min length: 2]
    + *
    note
    This field can be used to store a note that is not displayed to the customer. [max length: 4000]
    + *
    payment
    The ID of the payment. Use this ID to query the /payment API. [max length: 255]
    + *
    reference
    User defined reference field. [max length: 255]
    + *
    shippingAddress.city
    Address city of the location where the goods or services were supplied. [max length: 255, min length: 2]
    + *
    shippingAddress.country
    Address country of the location where the goods or services were supplied. [max length: 2, min length: 2]
    + *
    shippingAddress.line1
    Address line 1 of the location where the goods or services were supplied. [max length: 255]
    + *
    shippingAddress.line2
    Address line 2 of the location where the goods or services were supplied. [max length: 255]
    + *
    shippingAddress.name
    Address name of the location where the goods or services were supplied. [max length: 255]
    + *
    shippingAddress.state
    Address state of the location where the goods or services were supplied. [max length: 255]
    + *
    shippingAddress.zip
    Address zip of the location where the goods or services were supplied. [max length: 32]
    + *
    status
    New status of the invoice.
    + *
    suppliedDate
    The date on which the goods or services were supplied.
    + *
    taxNo
    The tax number or VAT id of the person who supplied the goods or services. [max length: 255]
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Invoice a Invoice object. + */ + public function updateInvoice($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "Invoice"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/InvoiceItem.php b/includes/gateways/simplify-commerce/includes/Simplify/InvoiceItem.php new file mode 100644 index 00000000000..9c7d9b8c053 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/InvoiceItem.php @@ -0,0 +1,128 @@ + + *
    amount
    Amount of the invoice item in the smallest unit of your currency. Example: 100 = $1.00USD [min value: -9999900, max value: 9999900] required
    + *
    description
    Individual items of an invoice [max length: 1024]
    + *
    invoice
    The ID of the invoice this item belongs to.
    + *
    product
    Product ID this item relates to.
    + *
    quantity
    Quantity of the item. This total amount of the invoice item is the amount * quantity. [min value: 1, max value: 999999, default: 1]
    + *
    reference
    User defined reference field. [max length: 255]
    + *
    tax
    The tax ID of the tax charge in the invoice item.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return InvoiceItem a InvoiceItem object. + */ + static public function createInvoiceItem($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_InvoiceItem(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_InvoiceItem object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteInvoiceItem($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve a Simplify_InvoiceItem object from the API + * + * @param string id the id of the InvoiceItem object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return InvoiceItem a InvoiceItem object + */ + static public function findInvoiceItem($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_InvoiceItem(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_InvoiceItem object. + * + * The properties that can be updated: + *
    + *
    amount
    Amount of the invoice item in the smallest unit of your currency. Example: 100 = $1.00USD [min value: 1]
    + *
    description
    Individual items of an invoice
    + *
    quantity
    Quantity of the item. This total amount of the invoice item is the amount * quantity. [min value: 1, max value: 999999]
    + *
    reference
    User defined reference field.
    + *
    tax
    The tax ID of the tax charge in the invoice item.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return InvoiceItem a InvoiceItem object. + */ + public function updateInvoiceItem($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "InvoiceItem"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Object.php b/includes/gateways/simplify-commerce/includes/Simplify/Object.php new file mode 100644 index 00000000000..f7fc4e42820 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Object.php @@ -0,0 +1,90 @@ +properties)) { + return $this->properties[$key]; + } else { + return null; + } + } + + /** + * @ignore + * + * @param string $key + * @param mixed $value + */ + public function __set($key, $value) { + $this->properties[$key] = $value; + } + + /** + * Updates the object's properties with the values in the specified map. + * @param $hash array Map of values to set. + */ + public function setAll($hash) { + foreach ($hash as $key => $value) { + $this->$key = $value; + } + } + + /** + * @ignore + */ + public function __toString() { + return json_encode($this->properties); + } + + /** + * Returns the object's properties as a map. + * @return array map of properties. + */ + public function getProperties() { + return $this->properties; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Payment.php b/includes/gateways/simplify-commerce/includes/Simplify/Payment.php new file mode 100644 index 00000000000..44f616992b3 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Payment.php @@ -0,0 +1,145 @@ + + *
    amount
    Amount of the payment (in the smallest unit of your currency). Example: 100 = $1.00USD
    + *
    authorization
    The ID of the authorization being used to capture the payment.
    + *
    card.addressCity
    City of the cardholder. [max length: 50, min length: 2]
    + *
    card.addressCountry
    Country code (ISO-3166-1-alpha-2 code) of residence of the cardholder. [max length: 2, min length: 2]
    + *
    card.addressLine1
    Address of the cardholder. [max length: 255]
    + *
    card.addressLine2
    Address of the cardholder if needed. [max length: 255]
    + *
    card.addressState
    State of residence of the cardholder. For the US, this is a 2-digit USPS code. [max length: 255, min length: 2]
    + *
    card.addressZip
    Postal code of the cardholder. The postal code size is between 5 and 9 in length and only contain numbers or letters. [max length: 9, min length: 3]
    + *
    card.cvc
    CVC security code of the card. This is the code on the back of the card. Example: 123
    + *
    card.expMonth
    Expiration month of the card. Format is MM. Example: January = 01 [min value: 1, max value: 12] required
    + *
    card.expYear
    Expiration year of the card. Format is YY. Example: 2013 = 13 [min value: 0, max value: 99] required
    + *
    card.name
    Name as it appears on the card. [max length: 50, min length: 2]
    + *
    card.number
    Card number as it appears on the card. [max length: 19, min length: 13] required
    + *
    currency
    Currency code (ISO-4217) for the transaction. Must match the currency associated with your account. [default: USD] required
    + *
    customer
    ID of customer. If specified, card on file of customer will be used.
    + *
    description
    Free form text field to be used as a description of the payment. This field is echoed back with the payment on any find or list operations. [max length: 1024]
    + *
    invoice
    ID of invoice for which this payment is being made.
    + *
    reference
    Custom reference field to be used with outside systems.
    + *
    replayId
    An identifier that can be sent to uniquely identify a payment request to facilitate retries due to I/O related issues. This identifier must be unique for your account (sandbox or live) across all of your payments. If supplied, we will check for a payment on your account that matches this identifier. If found will attempt to return an identical response of the original request. [max length: 50, min length: 1]
    + *
    statementDescription.name
    Merchant name. required
    + *
    statementDescription.phoneNumber
    Merchant contact phone number.
    + *
    token
    If specified, card associated with card token will be used. [max length: 255]
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Payment a Payment object. + */ + static public function createPayment($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Payment(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + + /** + * Retrieve Simplify_Payment objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: dateCreated createdBy amount id description paymentDate.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Payment objects and the total + * number of Payment objects available for the given criteria. + * @see ResourceList + */ + static public function listPayment($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Payment(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Payment object from the API + * + * @param string id the id of the Payment object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Payment a Payment object + */ + static public function findPayment($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Payment(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_Payment object. + * + * The properties that can be updated: + *
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Payment a Payment object. + */ + public function updatePayment($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "Payment"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/PaymentsApi.php b/includes/gateways/simplify-commerce/includes/Simplify/PaymentsApi.php new file mode 100644 index 00000000000..c6f2a894179 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/PaymentsApi.php @@ -0,0 +1,358 @@ + 'POST', + 'delete' => 'DELETE', + 'list' => 'GET', + 'show' => 'GET', + 'update' => 'PUT' + ); + + /** + * @ignore + * + * @param object $object + * @param object $authentication + * + * @return mixed + */ + static public function createObject($object, $authentication = null) + { + $paymentsApi = new Simplify_PaymentsApi(); + + $jsonObject = $paymentsApi->execute("create", $object, $authentication); + + $o = $paymentsApi->convertFromHashToObject($jsonObject, $object->getClazz()); + + return $o; + } + + /** + * @ignore + * + * @param object $object + * @param object $authentication + * + * @return mixed + */ + static public function findObject($object, $authentication = null) + { + $paymentsApi = new Simplify_PaymentsApi(); + + $jsonObject = $paymentsApi->execute("show", $object, $authentication); + $o = $paymentsApi->convertFromHashToObject($jsonObject, $object->getClazz()); + + return $o; + } + + /** + * @ignore + * + * @param object $object + * @param object $authentication + * + * @return mixed + */ + static public function updateObject($object, $authentication = null) { + $paymentsApi = new Simplify_PaymentsApi(); + + $jsonObject = $paymentsApi->execute("update", $object, $authentication); + $o = $paymentsApi->convertFromHashToObject($jsonObject, $object->getClazz()); + + return $o; + } + + /** + * @ignore + * + * @param object $object + * @param object $authentication + * + * @return mixed + */ + static public function deleteObject($object, $authentication = null) { + $paymentsApi = new Simplify_PaymentsApi(); + + $jsonObject = $paymentsApi->execute("delete", $object, $authentication); + + return $jsonObject; + } + + /** + * @ignore + * + * @param object $object + * @param array $criteria + * @param object $authentication + * + * @return Simplify_ResourceList + */ + static public function listObject($object, $criteria = null, $authentication = null) { + if ($criteria != null) { + if (isset($criteria['max'])) { + $object->max = $criteria['max']; + } + if (isset($criteria['offset'])) { + $object->offset = $criteria['offset']; + } + if (isset($criteria['sorting'])) { + $object->sorting = $criteria['sorting']; + } + if (isset($criteria['filter'])) { + $object->filter = $criteria['filter']; + } + } + + $paymentsApi = new Simplify_PaymentsApi(); + $jsonObject = $paymentsApi->execute("list", $object, $authentication); + + $ret = new Simplify_ResourceList(); + if (array_key_exists('list', $jsonObject) & is_array($jsonObject['list'])) { + foreach ($jsonObject['list'] as $obj) { + array_push($ret->list, $paymentsApi->convertFromHashToObject($obj, $object->getClazz())); + } + $ret->total = $jsonObject['total']; + } + + return $ret; + } + + /** + * @ignore + * + * @param array $from + * @param string $toClazz + * + * @return mixed + */ + public function convertFromHashToObject($from, $toClazz) + { + $clazz = 'stdClass'; + $toClazz = "Simplify_" . $toClazz; + if ("stdClass" != $toClazz && class_exists("{$toClazz}", false)) { + $clazz = "{$toClazz}"; + } + $object = new $clazz(); + + foreach ($from as $key => $value) { + if (is_array($value) && count(array_keys($value))) { + $newClazz = "Simplify_" . ucfirst($key); + if (!class_exists($newClazz, false)) { + $newClazz = 'stdClass'; + } + + $object->$key = $this->convertFromHashToObject($value, $newClazz); + } else { + $object->$key = $value; + } + } + + return $object; + } + + /** + * @ignore + * + * @param string $publicKey + * @param string $action + * @param object $object + * + * @return string + */ + public function getUrl($publicKey, $action, $object) + { + $url = $this->fixUrl(Simplify::$apiBaseSandboxUrl); + if ($this->isLiveKey($publicKey)) { + $url = $this->fixUrl(Simplify::$apiBaseLiveUrl); + } + $url = $this->fixUrl($url) . urlencode(lcfirst($object->getClazz())) . '/'; + + $queryParams = array(); + if ($action == "show") { + $url .= urlencode($object->id); + } elseif ($action == "list") { + $queryParams = array_merge($queryParams, array('max' => $object->max, 'offset' => $object->offset)); + if (is_array($object->filter) && count(array_keys($object->filter))) { + foreach ($object->filter as $key => $value) { + $queryParams["filter[$key]"] = $value; + } + } + if (is_array($object->sorting) && count(array_keys($object->sorting))) { + foreach ($object->sorting as $key => $value) { + $queryParams["sorting[$key]"] = $value; + } + } + $query = http_build_query($queryParams); + if ($query != '') { + if (strpos($url, '?', strlen($url)) === false) $url .= '?'; + $url .= $query; + } + + } elseif ($action == "delete") { + $url .= urlencode($object->id); + } elseif ($action == "update") { + $url .= urlencode($object->id); + } elseif ($action == "create") { + } + return $url; + } + + /** + * @ignore + * + * @param string $action + * + * @return string + */ + public function getMethod($action) + { + if (array_key_exists(strtolower($action), self::$methodMap)) { + return self::$methodMap[strtolower($action)]; + } + return 'GET'; + } + + /** + * @ignore + * + * @param string $action + * @param object $object + * @param object $authentication + * + * @return mixed + */ + private function execute($action, $object, $authentication) + { + $http = new Simplify_HTTP(); + + return $http->apiRequest($this->getUrl($authentication->publicKey, $action, $object), $this->getMethod($action), + $authentication, json_encode($object->getProperties())); + } + + /** + * @ignore + * + * @param string $hash + * @param object $authentication + * + * @return mixed + */ + public function jwsDecode($hash, $authentication) + { + $http = new Simplify_HTTP(); + + $data = $http->jwsDecode($authentication, $hash); + + return json_decode($data, true); + } + + /** + * @ignore + * + * @param string $url + * + * @return string + */ + private function fixUrl($url) + { + if ($this->endsWith($url, '/')) { + return $url; + } + return $url . '/'; + } + + /** + * @ignore + * + * @param string $k + * + * @return bool + */ + private function isLiveKey($k) { + return strpos($k, "lvpb") === 0; + } + + /** + * @ignore + * + * @param string $s + * @param string $c + * + * @return bool + */ + private function endsWith($s, $c) + { + return substr($s, -strlen($c)) == $c; + } + + /** + * Helper function to build the Authentication object for backwards compatibility. + * An array of all the arguments passed to one of the API functions is checked against what + * we expect to received. If it's greater, then we're assuming that the user is using the older way of + * passing the keys. i.e as two separate strings. We take those two string and create the Authentication object + * + * @ignore + * @param $authentication + * @param $args + * @param $expectedArgCount + * @return Simplify_Authentication + */ + static function buildAuthenticationObject($authentication = null, $args, $expectedArgCount){ + + if(sizeof($args) > $expectedArgCount) { + $authentication = new Simplify_Authentication($args[$expectedArgCount-1], $args[$expectedArgCount]); + } + + if ($authentication == null){ + $authentication = new Simplify_Authentication(); + } + + // check that the keys have been set, if not use the global keys + if ( empty($authentication->publicKey)){ + $authentication->publicKey = Simplify::$publicKey; + } + if ( empty($authentication->privateKey)){ + $authentication->privateKey = Simplify::$privateKey; + } + + return $authentication; + } + +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Plan.php b/includes/gateways/simplify-commerce/includes/Simplify/Plan.php new file mode 100644 index 00000000000..8f11103a098 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Plan.php @@ -0,0 +1,151 @@ + + *
    amount
    Amount of payment for the plan in the smallest unit of your currency. Example: 100 = $1.00USD required
    + *
    billingCycle
    How the plan is billed to the customer. Values must be AUTO (indefinitely until the customer cancels) or FIXED (a fixed number of billing cycles). [default: AUTO]
    + *
    billingCycleLimit
    The number of fixed billing cycles for a plan. Only used if the billingCycle parameter is set to FIXED. Example: 4
    + *
    currency
    Currency code (ISO-4217) for the plan. Must match the currency associated with your account. [default: USD] required
    + *
    frequency
    Frequency of payment for the plan. Used in conjunction with frequencyPeriod. Valid values are "DAILY", "WEEKLY", "MONTHLY" and "YEARLY". [default: MONTHLY] required
    + *
    frequencyPeriod
    Period of frequency of payment for the plan. Example: if the frequency is weekly, and periodFrequency is 2, then the subscription is billed bi-weekly. [min value: 1, default: 1] required
    + *
    name
    Name of the plan [max length: 50, min length: 2] required
    + *
    renewalReminderLeadDays
    If set, how many days before the next billing cycle that a renewal reminder is sent to the customer. If null, then no emails are sent. Minimum value is 7 if set.
    + *
    trialPeriod
    Plan free trial period selection. Must be Days, Weeks, or Month [default: NONE] required
    + *
    trialPeriodQuantity
    Quantity of the trial period. Must be greater than 0 and a whole number. [min value: 1]
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Plan a Plan object. + */ + static public function createPlan($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Plan(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_Plan object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deletePlan($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_Plan objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: dateCreated amount frequency name id.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Plan objects and the total + * number of Plan objects available for the given criteria. + * @see ResourceList + */ + static public function listPlan($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Plan(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Plan object from the API + * + * @param string id the id of the Plan object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Plan a Plan object + */ + static public function findPlan($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Plan(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_Plan object. + * + * The properties that can be updated: + *
    + *
    name
    Name of the plan. [min length: 2] required
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Plan a Plan object. + */ + public function updatePlan($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "Plan"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Refund.php b/includes/gateways/simplify-commerce/includes/Simplify/Refund.php new file mode 100644 index 00000000000..46c22ecfb87 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Refund.php @@ -0,0 +1,112 @@ + + *
    amount
    Amount of the refund in the smallest unit of your currency. Example: 100 = $1.00USD [min value: 1] required
    + *
    payment
    ID of the payment for the refund required
    + *
    reason
    Reason for the refund
    + *
    reference
    Custom reference field to be used with outside systems.
    + *
    replayId
    An identifier that can be sent to uniquely identify a refund request to facilitate retries due to I/O related issues. This identifier must be unique for your account (sandbox or live) across all of your refunds. If supplied, we will check for a refund on your account that matches this identifier. If found we will return an identical response to that of the original request. [max length: 50, min length: 1]
    + *
    statementDescription.name
    Merchant name. required
    + *
    statementDescription.phoneNumber
    Merchant contact phone number.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Refund a Refund object. + */ + static public function createRefund($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Refund(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + + /** + * Retrieve Simplify_Refund objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: id amount description dateCreated paymentDate.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Refund objects and the total + * number of Refund objects available for the given criteria. + * @see ResourceList + */ + static public function listRefund($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Refund(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Refund object from the API + * + * @param string id the id of the Refund object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Refund a Refund object + */ + static public function findRefund($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Refund(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + /** + * @ignore + */ + public function getClazz() { + return "Refund"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/ResourceList.php b/includes/gateways/simplify-commerce/includes/Simplify/ResourceList.php new file mode 100644 index 00000000000..7c31002fe96 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/ResourceList.php @@ -0,0 +1,48 @@ +() methods. + */ +class Simplify_ResourceList { + + /** + * @var array $list the list of domain objects. + */ + public $list = array(); + + /** + * @var int $total the total number of object available. + */ + public $total = 0; +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Subscription.php b/includes/gateways/simplify-commerce/includes/Simplify/Subscription.php new file mode 100644 index 00000000000..91d33005fa5 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Subscription.php @@ -0,0 +1,164 @@ + + *
    amount
    Amount of the payment in the smallest unit of your currency. Example: 100 = $1.00USD
    + *
    billingCycle
    How the plan is billed to the customer. Values must be AUTO (indefinitely until the customer cancels) or FIXED (a fixed number of billing cycles). [default: AUTO]
    + *
    billingCycleLimit
    The number of fixed billing cycles for a plan. Only used if the billingCycle parameter is set to FIXED. Example: 4
    + *
    coupon
    Coupon ID associated with the subscription
    + *
    currency
    Currency code (ISO-4217). Must match the currency associated with your account. [default: USD]
    + *
    customer
    Customer that is enrolling in the subscription.
    + *
    frequency
    Frequency of payment for the plan. Used in conjunction with frequencyPeriod. Valid values are "DAILY", "WEEKLY", "MONTHLY" and "YEARLY".
    + *
    frequencyPeriod
    Period of frequency of payment for the plan. Example: if the frequency is weekly, and periodFrequency is 2, then the subscription is billed bi-weekly.
    + *
    name
    Name describing subscription
    + *
    plan
    The ID of the plan that should be used for the subscription.
    + *
    quantity
    Quantity of the plan for the subscription. [min value: 1]
    + *
    renewalReminderLeadDays
    If set, how many days before the next billing cycle that a renewal reminder is sent to the customer. If null, then no emails are sent. Minimum value is 7 if set.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Subscription a Subscription object. + */ + static public function createSubscription($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Subscription(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_Subscription object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteSubscription($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_Subscription objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: id plan.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Subscription objects and the total + * number of Subscription objects available for the given criteria. + * @see ResourceList + */ + static public function listSubscription($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Subscription(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Subscription object from the API + * + * @param string id the id of the Subscription object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Subscription a Subscription object + */ + static public function findSubscription($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Subscription(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_Subscription object. + * + * The properties that can be updated: + *
    + *
    amount
    Amount of the payment in the smallest unit of your currency. Example: 100 = $1.00USD
    + *
    billingCycle
    How the plan is billed to the customer. Values must be AUTO (indefinitely until the customer cancels) or FIXED (a fixed number of billing cycles). [default: AUTO]
    + *
    billingCycleLimit
    The number of fixed billing cycles for a plan. Only used if the billingCycle parameter is set to FIXED. Example: 4
    + *
    coupon
    Coupon being assigned to this subscription
    + *
    currency
    Currency code (ISO-4217). Must match the currency associated with your account. [default: USD]
    + *
    frequency
    Frequency of payment for the plan. Used in conjunction with frequencyPeriod. Valid values are "DAILY", "WEEKLY", "MONTHLY" and "YEARLY".
    + *
    frequencyPeriod
    Period of frequency of payment for the plan. Example: if the frequency is weekly, and periodFrequency is 2, then the subscription is billed bi-weekly. [min value: 1]
    + *
    name
    Name describing subscription
    + *
    plan
    Plan that should be used for the subscription.
    + *
    prorate
    Whether to prorate existing subscription. [default: true] required
    + *
    quantity
    Quantity of the plan for the subscription. [min value: 1]
    + *
    renewalReminderLeadDays
    If set, how many days before the next billing cycle that a renewal reminder is sent to the customer. If null or 0, no emails are sent. Minimum value is 7 if set.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Subscription a Subscription object. + */ + public function updateSubscription($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "Subscription"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Tax.php b/includes/gateways/simplify-commerce/includes/Simplify/Tax.php new file mode 100644 index 00000000000..0e504afd5bf --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Tax.php @@ -0,0 +1,124 @@ + + *
    label
    The label of the tax object. [max length: 255] required
    + *
    rate
    The tax rate. Decimal value up three decimal places. e.g 12.501. [max length: 6] required
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Tax a Tax object. + */ + static public function createTax($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Tax(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_Tax object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteTax($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_Tax objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: id label.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Tax objects and the total + * number of Tax objects available for the given criteria. + * @see ResourceList + */ + static public function listTax($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Tax(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Tax object from the API + * + * @param string id the id of the Tax object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Tax a Tax object + */ + static public function findTax($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Tax(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + /** + * @ignore + */ + public function getClazz() { + return "Tax"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/TransactionReview.php b/includes/gateways/simplify-commerce/includes/Simplify/TransactionReview.php new file mode 100644 index 00000000000..29cd2187a3d --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/TransactionReview.php @@ -0,0 +1,141 @@ + + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return TransactionReview a TransactionReview object. + */ + static public function createTransactionReview($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_TransactionReview(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_TransactionReview object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteTransactionReview($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_TransactionReview objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Allows for ascending or descending sorting of the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Filters to apply to the list. [min value: 0, default: 0]
    + *
    sorting
    Used in paging of the list. This is the start offset of the page. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: dateCreated status.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of TransactionReview objects and the total + * number of TransactionReview objects available for the given criteria. + * @see ResourceList + */ + static public function listTransactionReview($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_TransactionReview(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_TransactionReview object from the API + * + * @param string id the id of the TransactionReview object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return TransactionReview a TransactionReview object + */ + static public function findTransactionReview($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_TransactionReview(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_TransactionReview object. + * + * The properties that can be updated: + *
    + *
    status
    Status of the transaction review.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return TransactionReview a TransactionReview object. + */ + public function updateTransactionReview($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "TransactionReview"; + } +} diff --git a/includes/gateways/simplify-commerce/includes/Simplify/Webhook.php b/includes/gateways/simplify-commerce/includes/Simplify/Webhook.php new file mode 100644 index 00000000000..efb80f487f8 --- /dev/null +++ b/includes/gateways/simplify-commerce/includes/Simplify/Webhook.php @@ -0,0 +1,142 @@ + + *
    url
    Endpoint URL required
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Webhook a Webhook object. + */ + static public function createWebhook($hash, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $instance = new Simplify_Webhook(); + $instance->setAll($hash); + + $object = Simplify_PaymentsApi::createObject($instance, $authentication); + return $object; + } + + + /** + * Deletes an Simplify_Webhook object. + * + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * + * @return true + */ + public function deleteWebhook($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $obj = Simplify_PaymentsApi::deleteObject($this, $authentication); + $this->properties = null; + return true; + } + + + /** + * Retrieve Simplify_Webhook objects. + * @param array criteria a map of parameters; valid keys are:
    + *
    filter
    Filters to apply to the list.
    + *
    max
    Allows up to a max of 50 list items to return. [min value: 0, max value: 50, default: 20]
    + *
    offset
    Used in paging of the list. This is the start offset of the page. [min value: 0, default: 0]
    + *
    sorting
    Allows for ascending or descending sorting of the list. The value maps properties to the sort direction (either asc for ascending or desc for descending). Sortable properties are: dateCreated.
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return ResourceList a ResourceList object that holds the list of Webhook objects and the total + * number of Webhook objects available for the given criteria. + * @see ResourceList + */ + static public function listWebhook($criteria = null, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Webhook(); + $list = Simplify_PaymentsApi::listObject($val, $criteria, $authentication); + + return $list; + } + + + /** + * Retrieve a Simplify_Webhook object from the API + * + * @param string id the id of the Webhook object to retrieve + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Webhook a Webhook object + */ + static public function findWebhook($id, $authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 2); + + $val = new Simplify_Webhook(); + $val->id = $id; + + $obj = Simplify_PaymentsApi::findObject($val, $authentication); + + return $obj; + } + + + /** + * Updates an Simplify_Webhook object. + * + * The properties that can be updated: + *
    + *
    url
    Endpoint URL required
    + * @param $authentication - information used for the API call. If no value is passed the global keys Simplify::public_key and Simplify::private_key are used. For backwards compatibility the public and private keys may be passed instead of the authentication object. + * @return Webhook a Webhook object. + */ + public function updateWebhook($authentication = null) { + + $args = func_get_args(); + $authentication = Simplify_PaymentsApi::buildAuthenticationObject($authentication, $args, 1); + + $object = Simplify_PaymentsApi::updateObject($this, $authentication); + return $object; + } + + /** + * @ignore + */ + public function getClazz() { + return "Webhook"; + } +} diff --git a/includes/import/abstract-wc-product-importer.php b/includes/import/abstract-wc-product-importer.php new file mode 100644 index 00000000000..1a8fea9e8cd --- /dev/null +++ b/includes/import/abstract-wc-product-importer.php @@ -0,0 +1,686 @@ +raw_keys; + } + + /** + * Get file mapped headers. + * + * @return array + */ + public function get_mapped_keys() { + return ! empty( $this->mapped_keys ) ? $this->mapped_keys : $this->raw_keys; + } + + /** + * Get raw data. + * + * @return array + */ + public function get_raw_data() { + return $this->raw_data; + } + + /** + * Get parsed data. + * + * @return array + */ + public function get_parsed_data() { + return apply_filters( 'woocommerce_product_importer_parsed_data', $this->parsed_data, $this->get_raw_data() ); + } + + /** + * Get file pointer position from the last read. + * + * @return int + */ + public function get_file_position() { + return $this->file_position; + } + + /** + * Get file pointer position as a percentage of file size. + * + * @return int + */ + public function get_percent_complete() { + $size = filesize( $this->file ); + if ( ! $size ) { + return 0; + } + + return absint( min( round( ( $this->file_position / $size ) * 100 ), 100 ) ); + } + + /** + * Prepare a single product for create or update. + * + * @param array $data Item data. + * @return WC_Product|WP_Error + */ + protected function get_product_object( $data ) { + $id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $data['type'] ) ) { + $types = array_keys( wc_get_product_types() ); + $types[] = 'variation'; + + if ( ! in_array( $data['type'], $types, true ) ) { + return new WP_Error( 'woocommerce_product_importer_invalid_type', __( 'Invalid product type.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $data['id'] ) ) { + $product = wc_get_product( $id ); + + if ( ! $product ) { + return new WP_Error( 'woocommerce_product_csv_importer_invalid_id', sprintf( __( 'Invalid product ID %d.', 'woocommerce' ), $id ), array( 'id' => $id, 'status' => 401 ) ); + } + } else { + $product = new WC_Product_Simple( $id ); + } + + return apply_filters( 'woocommerce_product_import_get_product_object', $product, $data ); + } + + /** + * Process a single item and save. + * + * @param array $data Raw CSV data. + * @return array|WC_Error + */ + protected function process_item( $data ) { + try { + do_action( 'woocommerce_product_import_before_process_item', $data ); + + // Get product ID from SKU if created during the importation. + if ( empty( $data['id'] ) && ! empty( $data['sku'] ) && ( $product_id = wc_get_product_id_by_sku( $data['sku'] ) ) ) { + $data['id'] = $product_id; + } + + $object = $this->get_product_object( $data ); + $updating = false; + + if ( is_wp_error( $object ) ) { + return $object; + } + + if ( $object->get_id() && 'importing' !== $object->get_status() ) { + $updating = true; + } + + if ( 'external' === $object->get_type() ) { + unset( $data['manage_stock'], $data['stock_status'], $data['backorders'] ); + } + + if ( 'importing' === $object->get_status() ) { + $object->set_status( 'publish' ); + $object->set_slug( '' ); + } + + $result = $object->set_props( array_diff_key( $data, array_flip( array( 'meta_data', 'raw_image_id', 'raw_gallery_image_ids', 'raw_attributes' ) ) ) ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + if ( 'variation' === $object->get_type() ) { + $this->set_variation_data( $object, $data ); + } else { + $this->set_product_data( $object, $data ); + } + + $this->set_image_data( $object, $data ); + $this->set_meta_data( $object, $data ); + + $object = apply_filters( 'woocommerce_product_import_pre_insert_product_object', $object, $data ); + $object->save(); + + do_action( 'woocommerce_product_import_inserted_product_object', $object, $data ); + + return array( + 'id' => $object->get_id(), + 'updated' => $updating, + ); + } catch ( Exception $e ) { + return new WP_Error( 'woocommerce_product_importer_error', $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Convert raw image URLs to IDs and set. + * + * @param WC_Product $product Product instance. + * @param array $data Item data. + */ + protected function set_image_data( &$product, $data ) { + // Image URLs need converting to IDs before inserting. + if ( isset( $data['raw_image_id'] ) ) { + $product->set_image_id( $this->get_attachment_id_from_url( $data['raw_image_id'], $product->get_id() ) ); + } + + // Gallery image URLs need converting to IDs before inserting. + if ( isset( $data['raw_gallery_image_ids'] ) ) { + $gallery_image_ids = array(); + + foreach ( $data['raw_gallery_image_ids'] as $image_id ) { + $gallery_image_ids[] = $this->get_attachment_id_from_url( $image_id, $product->get_id() ); + } + $product->set_gallery_image_ids( $gallery_image_ids ); + } + } + + /** + * Append meta data. + * + * @param WC_Product $product Product instance. + * @param array $data Item data. + */ + protected function set_meta_data( &$product, $data ) { + if ( isset( $data['meta_data'] ) ) { + foreach ( $data['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'] ); + } + } + } + + /** + * Set product data. + * + * @param WC_Product $product Product instance. + * @param array $data Item data. + * + * @return WC_Product|WP_Error + * @throws Exception + */ + protected function set_product_data( &$product, $data ) { + if ( isset( $data['raw_attributes'] ) ) { + $attributes = array(); + $default_attributes = array(); + + foreach ( $data['raw_attributes'] as $position => $attribute ) { + $attribute_id = 0; + + // Get ID if is a global attribute. + if ( ! empty( $attribute['taxonomy'] ) ) { + $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] ); + } + + // Set attribute visibility. + if ( isset( $attribute['visible'] ) ) { + $is_visible = $attribute['visible']; + } else { + $is_visible = 1; + } + + // Set if is a variation attribute. + $is_variation = 0; + + if ( $attribute_id ) { + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( isset( $attribute['value'] ) ) { + $options = array_map( 'wc_sanitize_term_text_based', $attribute['value'] ); + $options = array_filter( $options, 'strlen' ); + } else { + $options = array(); + } + + // Check for default attributes and set "is_variation". + if ( ! empty( $attribute['default'] ) && in_array( $attribute['default'], $options ) ) { + $default_term = get_term_by( 'name', $attribute['default'], $attribute_name ); + + if ( $default_term && ! is_wp_error( $default_term ) ) { + $default = $default_term->slug; + } else { + $default = sanitize_title( $attribute['default'] ); + } + + $default_attributes[ $attribute_name ] = $default; + $is_variation = 1; + } + + if ( ! empty( $options ) ) { + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $options ); + $attribute_object->set_position( $position ); + $attribute_object->set_visible( $is_visible ); + $attribute_object->set_variation( $is_variation ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['value'] ) ) { + // Check for default attributes and set "is_variation". + if ( ! empty( $attribute['default'] ) && in_array( $attribute['default'], $attribute['value'] ) ) { + $default_attributes[ sanitize_title( $attribute['name'] ) ] = $attribute['default']; + $is_variation = 1; + } + + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute['name'] ); + $attribute_object->set_options( $attribute['value'] ); + $attribute_object->set_position( $position ); + $attribute_object->set_visible( $is_visible ); + $attribute_object->set_variation( $is_variation ); + $attributes[] = $attribute_object; + } + } + + $product->set_attributes( $attributes ); + + // Set variable default attributes. + if ( $product->is_type( 'variable' ) ) { + $product->set_default_attributes( $default_attributes ); + } + } + } + + /** + * Set variation data. + * + * @param WC_Product $variation Product instance. + * @param array $data Item data. + * + * @return WC_Product|WP_Error + * @throws Exception + */ + protected function set_variation_data( &$variation, $data ) { + $parent = false; + + // Check if parent exist. + if ( isset( $data['parent_id'] ) ) { + $parent = wc_get_product( $data['parent_id'] ); + + if ( $parent ) { + $variation->set_parent_id( $parent->get_id() ); + } + } + + // Stop if parent does not exists. + if ( ! $parent ) { + return new WP_Error( 'woocommerce_product_importer_missing_variation_parent_id', __( 'Variation cannot be imported: Missing parent ID or parent does not exist yet.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + if ( isset( $data['raw_attributes'] ) ) { + $attributes = array(); + $parent_attributes = $this->get_variation_parent_attributes( $data['raw_attributes'], $parent ); + + foreach ( $data['raw_attributes'] as $attribute ) { + // Get ID if is a global attribute. + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); + + if ( $attribute_id ) { + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } else { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['value'] ) ? current( $attribute['value'] ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + } + + /** + * Get variation parent attributes and set "is_variation". + * + * @param array $attributes Attributes list. + * @param WC_Product $parent Parent product data. + * @return array + */ + protected function get_variation_parent_attributes( $attributes, $parent ) { + $parent_attributes = $parent->get_attributes(); + $require_save = false; + + foreach ( $attributes as $attribute ) { + $attribute_id = 0; + + // Get ID if is a global attribute. + if ( ! empty( $attribute['taxonomy'] ) ) { + $attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] ); + } + + if ( $attribute_id ) { + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } else { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + // Check if attribute handle variations. + if ( isset( $parent_attributes[ $attribute_name ] ) && ! $parent_attributes[ $attribute_name ]->get_variation() ) { + // Re-create the attribute to CRUD save and genarate again. + $parent_attributes[ $attribute_name ] = clone $parent_attributes[ $attribute_name ]; + $parent_attributes[ $attribute_name ]->set_variation( 1 ); + + $require_save = true; + } + } + + // Save variation attributes. + if ( $require_save ) { + $parent->set_attributes( array_values( $parent_attributes ) ); + $parent->save(); + } + + return $parent_attributes; + } + + /** + * Get attachment ID. + * + * @param string $url Attachment URL. + * @param int $product_id Product ID. + * @return int + */ + protected function get_attachment_id_from_url( $url, $product_id ) { + if ( empty( $url ) ) { + return 0; + } + + $id = 0; + $upload_dir = wp_upload_dir(); + $base_url = $upload_dir['baseurl'] . '/'; + + // Check first if attachment is on WordPress uploads directory. + if ( false !== strpos( $url, $base_url ) ) { + // Search for yyyy/mm/slug.extension + $file = str_replace( $base_url, '', $url ); + $args = array( + 'post_type' => 'attachment', + 'post_status' => 'any', + 'fields' => 'ids', + 'meta_query' => array( + array( + 'value' => $file, + 'compare' => 'LIKE', + 'key' => '_wp_attachment_metadata', + ), + ), + ); + + if ( $ids = get_posts( $args ) ) { + $id = current( $ids ); + } + } else { + $args = array( + 'post_type' => 'attachment', + 'post_status' => 'any', + 'fields' => 'ids', + 'meta_query' => array( + array( + 'value' => $url, + 'key' => '_wc_attachment_source', + ), + ), + ); + + if ( $ids = get_posts( $args ) ) { + $id = current( $ids ); + } + } + + // Upload if attachment does not exists. + if ( ! $id ) { + $upload = wc_rest_upload_image_from_url( $url ); + + if ( is_wp_error( $upload ) ) { + throw new Exception( $upload->get_error_message(), 400 ); + } + + $id = wc_rest_set_uploaded_image_as_attachment( $upload, $product_id ); + + if ( ! wp_attachment_is_image( $id ) ) { + throw new Exception( sprintf( __( 'Not able to attach "%s".', 'woocommerce' ), $url ), 400 ); + } + + // Save attachment source for future reference. + update_post_meta( $id, '_wc_attachment_source', $url ); + } + + return $id; + } + + /** + * Get attribute taxonomy ID from the imported data. + * If does not exists register a new attribute. + * + * @param string $name Attribute name. + * @return int + */ + protected function get_attribute_taxonomy_id( $raw_name ) { + global $wpdb, $wc_product_attributes; + + // These are exported as labels, so convert the label to a name if possible first. + $attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); + $attribute_name = ''; + + if ( ! $attribute_name = array_search( $raw_name, $attribute_labels ) ) { + $attribute_name = wc_sanitize_taxonomy_name( $raw_name ); + } + + // Get the ID from the name. + if ( $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name ) ) { + return $attribute_id; + } + + // If the attribute does not exist, create it. + $args = array( + 'attribute_label' => $raw_name, + 'attribute_name' => $attribute_name, + 'attribute_type' => 'select', + 'attribute_orderby' => 'menu_order', + 'attribute_public' => 0, + ); + + // Validate attribute. + if ( strlen( $attribute_name ) >= 28 ) { + throw new Exception( sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $attribute_name ), 400 ); + } elseif ( wc_check_if_attribute_name_is_reserved( $attribute_name ) ) { + throw new Exception( sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $attribute_name ), 400 ); + } elseif ( taxonomy_exists( wc_attribute_taxonomy_name( $attribute_name ) ) ) { + throw new Exception( sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $attribute_name ), 400 ); + } + + $result = $wpdb->insert( $wpdb->prefix . 'woocommerce_attribute_taxonomies', $args, array( '%s', '%s', '%s', '%s', '%d' ) ); + + // Pass errors. + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message(), 400 ); + } + + $attribute_id = absint( $wpdb->insert_id ); + + // Delete transient. + delete_transient( 'wc_attribute_taxonomies' ); + + // Register as taxonomy while importing. + register_taxonomy( wc_attribute_taxonomy_name( $attribute_name ), array( 'product' ), array( 'labels' => array( 'name' => $raw_name ) ) ); + + // Set product attributes global. + $wc_product_attributes = array(); + + foreach ( wc_get_attribute_taxonomies() as $tax ) { + $wc_product_attributes[ wc_attribute_taxonomy_name( $attribute_name ) ] = $tax; + } + + return $attribute_id; + } + + /** + * Memory exceeded + * + * Ensures the batch process never exceeds 90% + * of the maximum WordPress memory. + * + * @return bool + */ + protected function memory_exceeded() { + $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory + $current_memory = memory_get_usage( true ); + $return = false; + if ( $current_memory >= $memory_limit ) { + $return = true; + } + return apply_filters( 'woocommerce_product_importer_memory_exceeded', $return ); + } + + /** + * Get memory limit + * + * @return int + */ + protected function get_memory_limit() { + if ( function_exists( 'ini_get' ) ) { + $memory_limit = ini_get( 'memory_limit' ); + } else { + // Sensible default. + $memory_limit = '128M'; + } + + if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { + // Unlimited, set to 32GB. + $memory_limit = '32000M'; + } + return intval( $memory_limit ) * 1024 * 1024; + } + + /** + * Time exceeded. + * + * Ensures the batch never exceeds a sensible time limit. + * A timeout limit of 30s is common on shared hosting. + * + * @return bool + */ + protected function time_exceeded() { + $finish = $this->start_time + apply_filters( 'woocommerce_product_importer_default_time_limit', 20 ); // 20 seconds + $return = false; + if ( time() >= $finish ) { + $return = true; + } + return apply_filters( 'woocommerce_product_importer_time_exceeded', $return ); + } +} diff --git a/includes/import/class-wc-product-csv-importer.php b/includes/import/class-wc-product-csv-importer.php new file mode 100644 index 00000000000..6b72848de18 --- /dev/null +++ b/includes/import/class-wc-product-csv-importer.php @@ -0,0 +1,779 @@ + 0, // File pointer start. + 'end_pos' => -1, // File pointer end. + 'lines' => -1, // Max lines to read. + 'mapping' => array(), // Column mapping. csv_heading => schema_heading. + 'parse' => false, // Whether to sanitize and format data. + 'update_existing' => false, // Whether to update existing items. + 'delimiter' => ',', // CSV delimiter. + 'prevent_timeouts' => true, // Check memory and time usage and abort if reaching limit. + ); + + $this->params = wp_parse_args( $params, $default_args ); + $this->file = $file; + + $this->read_file(); + } + + /** + * Read file. + * + * @return array + */ + protected function read_file() { + if ( false !== ( $handle = fopen( $this->file, 'r' ) ) ) { + $this->raw_keys = fgetcsv( $handle, 0, $this->params['delimiter'] ); + + // Remove BOM signature from the first item. + if ( isset( $this->raw_keys[0] ) ) { + $this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] ); + } + + if ( 0 !== $this->params['start_pos'] ) { + fseek( $handle, (int) $this->params['start_pos'] ); + } + + while ( false !== ( $row = fgetcsv( $handle, 0, $this->params['delimiter'] ) ) ) { + $this->raw_data[] = $row; + $this->file_positions[ count( $this->raw_data ) ] = ftell( $handle ); + + if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) { + break; + } + } + + $this->file_position = ftell( $handle ); + } + + if ( ! empty( $this->params['mapping'] ) ) { + $this->set_mapped_keys(); + } + + if ( $this->params['parse'] ) { + $this->set_parsed_data(); + } + } + + /** + * Remove UTF-8 BOM signature. + * + * @param string $string String to handle. + * @return string + */ + protected function remove_utf8_bom( $string ) { + if ( 'efbbbf' === substr( bin2hex( $string ), 0, 6 ) ) { + $string = substr( $string, 3 ); + } + + return $string; + } + + /** + * Set file mapped keys. + * + * @return array + */ + protected function set_mapped_keys() { + $mapping = $this->params['mapping']; + + foreach ( $this->raw_keys as $key ) { + $this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key; + } + } + + /** + * Parse relative field and return product ID. + * + * Handles `id:xx` and SKUs. + * + * If mapping to an id: and the product ID does not exist, this link is not + * valid. + * + * If mapping to a SKU and the product ID does not exist, a temporary object + * will be created so it can be updated later. + * + * @param string $field Field value. + * @return int|string + */ + public function parse_relative_field( $field ) { + global $wpdb; + + if ( empty( $field ) ) { + return ''; + } + + if ( preg_match( '/^id:(\d+)$/', $field, $matches ) ) { + $id = intval( $matches[1] ); + $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); + + if ( $original_id ) { + return absint( $original_id ); + + // If we're not updating existing posts, we need a placeholder. + } elseif ( ! $this->params['update_existing'] ) { + $product = new WC_Product_Simple(); + $product->set_name( 'Import placeholder for ' . $id ); + $product->set_status( 'importing' ); + $product->add_meta_data( '_original_id', $id, true ); + $id = $product->save(); + } + + return $id; + } + + if ( $id = wc_get_product_id_by_sku( $field ) ) { + return $id; + } + + try { + $product = new WC_Product_Simple(); + $product->set_name( 'Import placeholder for ' . $field ); + $product->set_status( 'importing' ); + $product->set_sku( $field ); + $id = $product->save(); + + if ( $id && ! is_wp_error( $id ) ) { + return $id; + } + } catch ( Exception $e ) { + return ''; + } + + return ''; + } + + /** + * Parse the ID field. + * + * If we're not doing an update, create a placeholder product so mapping works + * for rows following this one. + * + * @return int + */ + public function parse_id_field( $field ) { + global $wpdb; + + $id = absint( $field ); + + if ( ! $id ) { + return 0; + } + + // See if this maps to an ID placeholder already. + $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); + + if ( $original_id ) { + return absint( $original_id ); + } + + // Not updating? Make sure we have a new placeholder for this ID. + if ( ! $this->params['update_existing'] ) { + $product = new WC_Product_Simple(); + $product->set_name( 'Import placeholder for ' . $id ); + $product->set_status( 'importing' ); + $product->add_meta_data( '_original_id', $id, true ); + $id = $product->save(); + } + + return $id && ! is_wp_error( $id ) ? $id : 0; + } + + /** + * Parse reletive comma-delineated field and return product ID. + * + * @param string $field Field value. + * @return array + */ + public function parse_relative_comma_field( $field ) { + if ( empty( $field ) ) { + return array(); + } + + return array_filter( array_map( array( $this, 'parse_relative_field' ), array_map( 'trim', explode( ',', $field ) ) ) ); + } + + /** + * Parse a comma-delineated field from a CSV. + * + * @param string $field Field value. + * @return array + */ + public function parse_comma_field( $field ) { + if ( empty( $field ) ) { + return array(); + } + + return array_map( 'wc_clean', array_map( 'trim', explode( ',', $field ) ) ); + } + + /** + * Parse a field that is generally '1' or '0' but can be something else. + * + * @param string $field Field value. + * @return bool|string + */ + public function parse_bool_field( $field ) { + if ( '0' === $field ) { + return false; + } + + if ( '1' === $field ) { + return true; + } + + // Don't return explicit true or false for empty fields or values like 'notify'. + return wc_clean( $field ); + } + + /** + * Parse a float value field. + * + * @param string $field Field value. + * @return float|string + */ + public function parse_float_field( $field ) { + if ( '' === $field ) { + return $field; + } + + return floatval( $field ); + } + + /** + * Parse a category field from a CSV. + * Categories are separated by commas and subcategories are "parent > subcategory". + * + * @param string $field Field value. + * @return array of arrays with "parent" and "name" keys. + */ + public function parse_categories_field( $field ) { + if ( empty( $field ) ) { + return array(); + } + + $row_terms = array_map( 'trim', explode( ',', $field ) ); + $categories = array(); + + foreach ( $row_terms as $row_term ) { + $parent = null; + $_terms = array_map( 'trim', explode( '>', $row_term ) ); + $total = count( $_terms ); + + foreach ( $_terms as $index => $_term ) { + // Check if category exists. Parent must be empty string or null if doesn't exists. + // @codingStandardsIgnoreStart + $term = term_exists( $_term, 'product_cat', $parent ); + // @codingStandardsIgnoreEnd + + if ( is_array( $term ) ) { + $term_id = $term['term_id']; + } else { + $term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) ); + $term_id = $term['term_id']; + } + + // Only requires assign the last category. + if ( ( 1 + $index ) === $total ) { + $categories[] = $term_id; + } else { + // Store parent to be able to insert or query categories based in parent ID. + $parent = $term_id; + } + } + } + + return $categories; + } + + /** + * Parse a tag field from a CSV. + * + * @param string $field Field value. + * @return array + */ + public function parse_tags_field( $field ) { + if ( empty( $field ) ) { + return array(); + } + + $names = array_map( 'trim', explode( ',', $field ) ); + $tags = array(); + + foreach ( $names as $name ) { + $term = get_term_by( 'name', $name, 'product_tag' ); + + if ( ! $term || is_wp_error( $term ) ) { + $term = (object) wp_insert_term( $name, 'product_tag' ); + } + + $tags[] = $term->term_id; + } + + return $tags; + } + + /** + * Parse a shipping class field from a CSV. + * + * @param string $field Field value. + * @return int + */ + public function parse_shipping_class_field( $field ) { + if ( empty( $field ) ) { + return 0; + } + + $term = get_term_by( 'name', $field, 'product_shipping_class' ); + + if ( ! $term || is_wp_error( $term ) ) { + $term = (object) wp_insert_term( $field, 'product_shipping_class' ); + } + + return $term->term_id; + } + + /** + * Parse images list from a CSV. + * + * @param string $field Field value. + * @return array + */ + public function parse_images_field( $field ) { + if ( empty( $field ) ) { + return array(); + } + + return array_map( 'esc_url_raw', array_map( 'trim', explode( ',', $field ) ) ); + } + + /** + * Parse dates from a CSV. + * Dates requires the format YYYY-MM-DD. + * + * @param string $field Field value. + * @return string|null + */ + public function parse_date_field( $field ) { + if ( empty( $field ) ) { + return null; + } + + if ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/', $field ) ) { + return $field; + } + + return null; + } + + /** + * Parse backorders from a CSV. + * + * @param string $field Field value. + * @return string + */ + public function parse_backorders_field( $field ) { + if ( empty( $field ) ) { + return ''; + } + + $field = $this->parse_bool_field( $field ); + + if ( 'notify' === $field ) { + return 'notify'; + } elseif ( is_bool( $field ) ) { + return $field ? 'yes' : 'no'; + } + + return ''; + } + + /** + * Get formatting callback. + * + * @return array + */ + protected function get_formating_callback() { + + /** + * Columns not mentioned here will get parsed with 'wc_clean'. + * column_name => callback. + */ + $data_formatting = array( + 'id' => array( $this, 'parse_id_field' ), + 'type' => array( $this, 'parse_comma_field' ), + 'published' => array( $this, 'parse_bool_field' ), + 'featured' => array( $this, 'parse_bool_field' ), + 'date_on_sale_from' => array( $this, 'parse_date_field' ), + 'date_on_sale_to' => array( $this, 'parse_date_field' ), + 'name' => 'wp_filter_post_kses', + 'short_description' => 'wp_filter_post_kses', + 'description' => 'wp_filter_post_kses', + 'manage_stock' => array( $this, 'parse_bool_field' ), + 'backorders' => array( $this, 'parse_backorders_field' ), + 'stock_status' => array( $this, 'parse_bool_field' ), + 'sold_individually' => array( $this, 'parse_bool_field' ), + 'width' => array( $this, 'parse_float_field' ), + 'length' => array( $this, 'parse_float_field' ), + 'height' => array( $this, 'parse_float_field' ), + 'weight' => array( $this, 'parse_float_field' ), + 'reviews_allowed' => array( $this, 'parse_bool_field' ), + 'purchase_note' => 'wp_filter_post_kses', + 'price' => 'wc_format_decimal', + 'regular_price' => 'wc_format_decimal', + 'stock_quantity' => 'wc_stock_amount', + 'category_ids' => array( $this, 'parse_categories_field' ), + 'tag_ids' => array( $this, 'parse_tags_field' ), + 'shipping_class_id' => array( $this, 'parse_shipping_class_field' ), + 'images' => array( $this, 'parse_images_field' ), + 'parent_id' => array( $this, 'parse_relative_field' ), + 'grouped_products' => array( $this, 'parse_relative_comma_field' ), + 'upsell_ids' => array( $this, 'parse_relative_comma_field' ), + 'cross_sell_ids' => array( $this, 'parse_relative_comma_field' ), + 'download_limit' => 'absint', + 'download_expiry' => 'absint', + 'product_url' => 'esc_url_raw', + ); + + /** + * Match special column names. + */ + $regex_match_data_formatting = array( + '/attributes:value*/' => array( $this, 'parse_comma_field' ), + '/attributes:visible*/' => array( $this, 'parse_bool_field' ), + '/attributes:taxonomy*/' => array( $this, 'parse_bool_field' ), + '/downloads:url*/' => 'esc_url', + '/meta:*/' => 'wp_kses_post', // Allow some HTML in meta fields. + ); + + $callbacks = array(); + + // Figure out the parse function for each column. + foreach ( $this->get_mapped_keys() as $index => $heading ) { + $callback = 'wc_clean'; + + if ( isset( $data_formatting[ $heading ] ) ) { + $callback = $data_formatting[ $heading ]; + } else { + foreach ( $regex_match_data_formatting as $regex => $callback ) { + if ( preg_match( $regex, $heading ) ) { + $callback = $callback; + break; + } + } + } + + $callbacks[] = $callback; + } + + return apply_filters( 'woocommerce_product_importer_formatting_callbacks', $callbacks, $this ); + } + + /** + * Check if strings starts with determined word. + * + * @param string $haystack Complete sentence. + * @param string $needle Excerpt. + * @return bool + */ + protected function starts_with( $haystack, $needle ) { + return substr( $haystack, 0, strlen( $needle ) ) === $needle; + } + + /** + * Expand special and internal data into the correct formats for the product CRUD. + * + * @param array $data Data to import. + * @return array + */ + protected function expand_data( $data ) { + $data = apply_filters( 'woocommerce_product_importer_pre_expand_data', $data ); + + // Status is mapped from a special published field. + if ( isset( $data['published'] ) ) { + $data['status'] = ( $data['published'] ? 'publish' : 'draft' ); + unset( $data['published'] ); + } + + // Images field maps to image and gallery id fields. + if ( isset( $data['images'] ) ) { + $images = $data['images']; + $data['raw_image_id'] = array_shift( $images ); + + if ( ! empty( $images ) ) { + $data['raw_gallery_image_ids'] = $images; + } + unset( $data['images'] ); + } + + // Type, virtual and downloadable are all stored in the same column. + if ( isset( $data['type'] ) ) { + $data['type'] = array_map( 'strtolower', $data['type'] ); + $data['virtual'] = in_array( 'virtual', $data['type'], true ); + $data['downloadable'] = in_array( 'downloadable', $data['type'], true ); + + // Convert type to string. + $data['type'] = current( array_diff( $data['type'], array( 'virtual', 'downloadable' ) ) ); + } + + if ( isset( $data['stock_quantity'] ) ) { + $data['manage_stock'] = 0 < $data['stock_quantity']; + } + + // Stock is bool. + if ( isset( $data['stock_status'] ) ) { + $data['stock_status'] = $data['stock_status'] ? 'instock' : 'outofstock'; + } + + // Prepare grouped products. + if ( isset( $data['grouped_products'] ) ) { + $data['children'] = $data['grouped_products']; + unset( $data['grouped_products'] ); + } + + // Handle special column names which span multiple columns. + $attributes = array(); + $downloads = array(); + $meta_data = array(); + + foreach ( $data as $key => $value ) { + // Attributes. + if ( $this->starts_with( $key, 'attributes:name' ) ) { + if ( ! empty( $value ) ) { + $attributes[ str_replace( 'attributes:name', '', $key ) ]['name'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:value' ) ) { + $attributes[ str_replace( 'attributes:value', '', $key ) ]['value'] = $value; + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:taxonomy' ) ) { + $attributes[ str_replace( 'attributes:taxonomy', '', $key ) ]['taxonomy'] = wc_string_to_bool( $value ); + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:visible' ) ) { + $attributes[ str_replace( 'attributes:visible', '', $key ) ]['visible'] = wc_string_to_bool( $value ); + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:default' ) ) { + if ( ! empty( $value ) ) { + $attributes[ str_replace( 'attributes:default', '', $key ) ]['default'] = $value; + } + unset( $data[ $key ] ); + + // Downloads. + } elseif ( $this->starts_with( $key, 'downloads:name' ) ) { + if ( ! empty( $value ) ) { + $downloads[ str_replace( 'downloads:name', '', $key ) ]['name'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'downloads:url' ) ) { + if ( ! empty( $value ) ) { + $downloads[ str_replace( 'downloads:url', '', $key ) ]['url'] = $value; + } + unset( $data[ $key ] ); + + // Meta data. + } elseif ( $this->starts_with( $key, 'meta:' ) ) { + $meta_data[] = array( + 'key' => str_replace( 'meta:', '', $key ), + 'value' => $value, + ); + unset( $data[ $key ] ); + } + } + + if ( ! empty( $attributes ) ) { + // Remove empty attributes and clear indexes. + foreach ( $attributes as $attribute ) { + if ( empty( $attribute['name'] ) ) { + continue; + } + + $data['raw_attributes'][] = $attribute; + } + } + + if ( ! empty( $downloads ) ) { + $data['downloads'] = array(); + + foreach ( $downloads as $key => $file ) { + if ( empty( $file['url'] ) ) { + continue; + } + + $data['downloads'][] = array( + 'name' => $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['url'] ), + 'file' => $file['url'], + ); + } + } + + if ( ! empty( $meta_data ) ) { + $data['meta_data'] = $meta_data; + } + + return $data; + } + + /** + * Map and format raw data to known fields. + * + * @return array + */ + protected function set_parsed_data() { + $parse_functions = $this->get_formating_callback(); + $mapped_keys = $this->get_mapped_keys(); + + // Parse the data. + foreach ( $this->raw_data as $row ) { + // Skip empty rows. + if ( ! count( array_filter( $row ) ) ) { + continue; + } + $data = array(); + + do_action( 'woocommerce_product_importer_before_set_parsed_data', $row, $mapped_keys ); + + foreach ( $row as $id => $value ) { + // Skip ignored columns. + if ( empty( $mapped_keys[ $id ] ) ) { + continue; + } + + $data[ $mapped_keys[ $id ] ] = call_user_func( $parse_functions[ $id ], $value ); + } + + $this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this ); + } + } + + /** + * Get a string to identify the row from parsed data. + * + * @param array $parsed_data + * @return string + */ + protected function get_row_id( $parsed_data ) { + $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; + $sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : ''; + $name = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : ''; + $row_data = array(); + + if ( $name ) { + $row_data[] = $name; + } + if ( $id ) { + $row_data[] = sprintf( __( 'ID %d', 'woocommerce' ), $id ); + } + if ( $sku ) { + $row_data[] = sprintf( __( 'SKU %s', 'woocommerce' ), $sku ); + } + + return implode( ', ', $row_data ); + } + + /** + * Process importer. + * + * Do not import products with IDs or SKUs that already exist if option + * update existing is false, and likewise, if updating products, do not + * process rows which do not exist if an ID/SKU is provided. + * + * @return array + */ + public function import() { + $this->start_time = time(); + $index = 0; + $update_existing = $this->params['update_existing']; + $data = array( + 'imported' => array(), + 'failed' => array(), + 'updated' => array(), + 'skipped' => array(), + ); + + foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) { + $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; + $sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : ''; + $id_exists = false; + $sku_exists = false; + + if ( $id ) { + $product = wc_get_product( $id ); + $id_exists = $product && 'importing' !== $product->get_status(); + } elseif ( $sku && ( $id_from_sku = wc_get_product_id_by_sku( $sku ) ) ) { + $product = wc_get_product( $id_from_sku ); + $sku_exists = $product && 'importing' !== $product->get_status(); + } + + if ( $id_exists && ! $update_existing ) { + $data['skipped'][] = new WP_Error( 'woocommerce_product_importer_error', __( 'A product with this ID already exists.', 'woocommerce' ), array( 'id' => $id, 'row' => $this->get_row_id( $parsed_data ) ) ); + continue; + } + + if ( $sku_exists && ! $update_existing ) { + $data['skipped'][] = new WP_Error( 'woocommerce_product_importer_error', __( 'A product with this SKU already exists.', 'woocommerce' ), array( 'sku' => $sku, 'row' => $this->get_row_id( $parsed_data ) ) ); + continue; + } + + if ( $update_existing && ( $id || $sku ) && ! $id_exists && ! $sku_exists ) { + $data['skipped'][] = new WP_Error( 'woocommerce_product_importer_error', __( 'No matching product exists to update.', 'woocommerce' ), array( 'id' => $id, 'sku' => $sku, 'row' => $this->get_row_id( $parsed_data ) ) ); + continue; + } + + $result = $this->process_item( $parsed_data ); + + if ( is_wp_error( $result ) ) { + $result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) ); + $data['failed'][] = $result; + } elseif ( $result['updated'] ) { + $data['updated'][] = $result['id']; + } else { + $data['imported'][] = $result['id']; + } + + $index ++; + + if ( $this->params['prevent_timeouts'] && ( $this->time_exceeded() || $this->memory_exceeded() ) ) { + $this->file_position = $this->file_positions[ $index ]; + break; + } + } + + return $data; + } +} diff --git a/includes/interfaces/class-wc-abstract-order-data-store-interface.php b/includes/interfaces/class-wc-abstract-order-data-store-interface.php new file mode 100644 index 00000000000..a511b705661 --- /dev/null +++ b/includes/interfaces/class-wc-abstract-order-data-store-interface.php @@ -0,0 +1,49 @@ + [], 'failed' => []] + * + * @return array + */ + public function import(); + + /** + * Get file raw keys. + * + * CSV - Headers. + * XML - Element names. + * JSON - Keys + * + * @return array + */ + public function get_raw_keys(); + + /** + * Get file mapped headers. + * + * @return array + */ + public function get_mapped_keys(); + + /** + * Get raw data. + * + * @return array + */ + public function get_raw_data(); + + /** + * Get parsed data. + * + * @return array + */ + public function get_parsed_data(); + + /** + * Get file pointer position from the last read. + * + * @return int + */ + public function get_file_position(); + + /** + * Get file pointer position as a percentage of file size. + * + * @return int + */ + public function get_percent_complete(); +} diff --git a/includes/interfaces/class-wc-log-handler-interface.php b/includes/interfaces/class-wc-log-handler-interface.php new file mode 100644 index 00000000000..5bcd92934ad --- /dev/null +++ b/includes/interfaces/class-wc-log-handler-interface.php @@ -0,0 +1,28 @@ +id) + * @return array + */ + public function delete_meta( &$data, $meta ); + + /** + * Add new piece of meta. + * @param WC_Data &$data + * @param object $meta (containing ->key and ->value) + * @return int meta ID + */ + public function add_meta( &$data, $meta ); + + /** + * Update meta. + * @param WC_Data &$data + * @param object $meta (containing ->id, ->key and ->value) + */ + public function update_meta( &$data, $meta ); +} diff --git a/includes/interfaces/class-wc-order-data-store-interface.php b/includes/interfaces/class-wc-order-data-store-interface.php new file mode 100644 index 00000000000..2928aee9651 --- /dev/null +++ b/includes/interfaces/class-wc-order-data-store-interface.php @@ -0,0 +1,126 @@ +get_id() will be set. + * + * @param WC_Order_Item $item + */ + public function save_item_data( &$item ); +} diff --git a/includes/interfaces/class-wc-order-refund-data-store-interface.php b/includes/interfaces/class-wc-order-refund-data-store-interface.php new file mode 100644 index 00000000000..b21622d961e --- /dev/null +++ b/includes/interfaces/class-wc-order-refund-data-store-interface.php @@ -0,0 +1,16 @@ +id, $return[0]->parent_id. + * + * @return array + */ + public function get_on_sale_products(); + + /** + * Returns a list of product IDs ( id as key => parent as value) that are + * featured. Uses get_posts instead of wc_get_products since we want + * some extra meta queries and ALL products (posts_per_page = -1). + * + * @return array + */ + public function get_featured_product_ids(); + + /** + * Check if product sku is found for any other product IDs. + * + * @param int $product_id + * @param string $sku + * @return bool + */ + public function is_existing_sku( $product_id, $sku ); + + /** + * Return product ID based on SKU. + * + * @param string $sku + * @return int + */ + public function get_product_id_by_sku( $sku ); + + /** + * Returns an array of IDs of products that have sales starting soon. + * + * @return array + */ + public function get_starting_sales(); + + /** + * Returns an array of IDs of products that have sales which are due to end. + * + * @return array + */ + public function get_ending_sales(); + + /** + * Find a matching (enabled) variation within a variable product. + * + * @param WC_Product $product Variable product. + * @param array $match_attributes Array of attributes we want to try to match. + * @return int Matching variation ID or 0. + */ + public function find_matching_product_variation( $product, $match_attributes = array() ); + + /** + * Make sure all variations have a sort order set so they can be reordered correctly. + * + * @param int $parent_id + */ + public function sort_all_product_variations( $parent_id ); + + /** + * Return a list of related products (using data like categories and IDs). + * + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * @param int $product_id + * @return array + */ + public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ); + + /** + * Update a product's stock amount directly. + * + * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). + * + * @param int $product_id_with_stock + * @param int|null $stock_quantity + * @param string $operation set, increase and decrease. + */ + public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ); + + /** + * Update a product's sale count directly. + * + * Uses queries rather than update_post_meta so we can do this in one query for performance. + * + * @since 3.0.0 this supports set, increase and decrease. + * @param int $product_id + * @param int|null $quantity + * @param string $operation set, increase and decrease. + */ + public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ); + + /** + * Get shipping class ID by slug. + * @param string $slug + * @return int|false + */ + public function get_shipping_class_id_by_slug( $slug ); + + /** + * Returns an array of products. + * + * @param array $args @see wc_get_products + * @return array + */ + public function get_products( $args = array() ); + + /** + * Get the product type based on product ID. + * @param int $product_id + * @return bool|string + */ + public function get_product_type( $product_id ); +} diff --git a/includes/interfaces/class-wc-product-variable-data-store-interface.php b/includes/interfaces/class-wc-product-variable-data-store-interface.php new file mode 100644 index 00000000000..b327abfb4de --- /dev/null +++ b/includes/interfaces/class-wc-product-variable-data-store-interface.php @@ -0,0 +1,79 @@ +set_props( array( + 'code' => $code, + 'discount' => $discount, + 'discount_tax' => $discount_tax, + 'order_id' => $this->get_id(), + ) ); + $item->save(); + $this->add_item( $item ); + wc_do_deprecated_action( 'woocommerce_order_add_coupon', array( $this->get_id(), $item->get_id(), $code, $discount, $discount_tax ), '3.0', 'woocommerce_new_order_item action instead.' ); + return $item->get_id(); + } + + /** + * Add a tax row to the order. + * @param int $tax_rate_id + * @param int $tax_amount amount of tax. + * @param int $shipping_tax_amount shipping amount. + * @return int order item ID + * @throws WC_Data_Exception + */ + public function add_tax( $tax_rate_id, $tax_amount = 0, $shipping_tax_amount = 0 ) { + wc_deprecated_function( 'WC_Order::add_tax', '3.0', 'a new WC_Order_Item_Tax object and add to order with WC_Order::add_item()' ); + + $item = new WC_Order_Item_Tax(); + $item->set_props( array( + 'rate_id' => $tax_rate_id, + 'tax_total' => $tax_amount, + 'shipping_tax_total' => $shipping_tax_amount, + ) ); + $item->set_rate( $tax_rate_id ); + $item->set_order_id( $this->get_id() ); + $item->save(); + $this->add_item( $item ); + wc_do_deprecated_action( 'woocommerce_order_add_tax', array( $this->get_id(), $item->get_id(), $tax_rate_id, $tax_amount, $shipping_tax_amount ), '3.0', 'woocommerce_new_order_item action instead.' ); + return $item->get_id(); + } + + /** + * Add a shipping row to the order. + * @param WC_Shipping_Rate shipping_rate + * @return int order item ID + * @throws WC_Data_Exception + */ + public function add_shipping( $shipping_rate ) { + wc_deprecated_function( 'WC_Order::add_shipping', '3.0', 'a new WC_Order_Item_Shipping object and add to order with WC_Order::add_item()' ); + + $item = new WC_Order_Item_Shipping(); + $item->set_props( array( + 'method_title' => $shipping_rate->label, + 'method_id' => $shipping_rate->id, + 'total' => wc_format_decimal( $shipping_rate->cost ), + 'taxes' => $shipping_rate->taxes, + 'order_id' => $this->get_id(), + ) ); + foreach ( $shipping_rate->get_meta_data() as $key => $value ) { + $item->add_meta_data( $key, $value, true ); + } + $item->save(); + $this->add_item( $item ); + wc_do_deprecated_action( 'woocommerce_order_add_shipping', array( $this->get_id(), $item->get_id(), $shipping_rate ), '3.0', 'woocommerce_new_order_item action instead.' ); + return $item->get_id(); + } + + /** + * Add a fee to the order. + * Order must be saved prior to adding items. + * + * Fee is an amount of money charged for a particular piece of work + * or for a particular right or service, and not supposed to be negative. + * + * @throws WC_Data_Exception + * @param object $fee Fee data. + * @return int Updated order item ID. + */ + public function add_fee( $fee ) { + wc_deprecated_function( 'WC_Order::add_fee', '3.0', 'a new WC_Order_Item_Fee object and add to order with WC_Order::add_item()' ); + + $item = new WC_Order_Item_Fee(); + $item->set_props( array( + 'name' => $fee->name, + 'tax_class' => $fee->taxable ? $fee->tax_class : 0, + 'total' => $fee->amount, + 'total_tax' => $fee->tax, + 'taxes' => array( + 'total' => $fee->tax_data, + ), + 'order_id' => $this->get_id(), + ) ); + $item->save(); + $this->add_item( $item ); + wc_do_deprecated_action( 'woocommerce_order_add_fee', array( $this->get_id(), $item->get_id(), $fee ), '3.0', 'woocommerce_new_order_item action instead.' ); + return $item->get_id(); + } + + /** + * Update a line item for the order. + * + * Note this does not update order totals. + * + * @param object|int $item order item ID or item object. + * @param WC_Product $product + * @param array $args data to update. + * @return int updated order item ID + * @throws WC_Data_Exception + */ + public function update_product( $item, $product, $args ) { + wc_deprecated_function( 'WC_Order::update_product', '3.0', 'an interaction with the WC_Order_Item_Product class' ); + if ( is_numeric( $item ) ) { + $item = $this->get_item( $item ); + } + if ( ! is_object( $item ) || ! $item->is_type( 'line_item' ) ) { + return false; + } + if ( ! $this->get_id() ) { + $this->save(); // Order must exist + } + + // BW compatibility with old args + if ( isset( $args['totals'] ) ) { + foreach ( $args['totals'] as $key => $value ) { + if ( 'tax' === $key ) { + $args['total_tax'] = $value; + } elseif ( 'tax_data' === $key ) { + $args['taxes'] = $value; + } else { + $args[ $key ] = $value; + } + } + } + + // Handly qty if set + if ( isset( $args['qty'] ) ) { + if ( $product->backorders_require_notification() && $product->is_on_backorder( $args['qty'] ) ) { + $item->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ) ), $args['qty'] - max( 0, $product->get_stock_quantity() ), true ); + } + $args['subtotal'] = $args['subtotal'] ? $args['subtotal'] : wc_get_price_excluding_tax( $product, array( 'qty' => $args['qty'] ) ); + $args['total'] = $args['total'] ? $args['total'] : wc_get_price_excluding_tax( $product, array( 'qty' => $args['qty'] ) ); + } + + $item->set_order_id( $this->get_id() ); + $item->set_props( $args ); + $item->save(); + do_action( 'woocommerce_order_edit_product', $this->get_id(), $item->get_id(), $args, $product ); + + return $item->get_id(); + } + + /** + * Update coupon for order. Note this does not update order totals. + * @param object|int $item + * @param array $args + * @return int updated order item ID + * @throws WC_Data_Exception + */ + public function update_coupon( $item, $args ) { + wc_deprecated_function( 'WC_Order::update_coupon', '3.0', 'an interaction with the WC_Order_Item_Coupon class' ); + if ( is_numeric( $item ) ) { + $item = $this->get_item( $item ); + } + if ( ! is_object( $item ) || ! $item->is_type( 'coupon' ) ) { + return false; + } + if ( ! $this->get_id() ) { + $this->save(); // Order must exist + } + + // BW compatibility for old args + if ( isset( $args['discount_amount'] ) ) { + $args['discount'] = $args['discount_amount']; + } + if ( isset( $args['discount_amount_tax'] ) ) { + $args['discount_tax'] = $args['discount_amount_tax']; + } + + $item->set_order_id( $this->get_id() ); + $item->set_props( $args ); + $item->save(); + + do_action( 'woocommerce_order_update_coupon', $this->get_id(), $item->get_id(), $args ); + + return $item->get_id(); + } + + /** + * Update shipping method for order. + * + * Note this does not update the order total. + * + * @param object|int $item + * @param array $args + * @return int updated order item ID + * @throws WC_Data_Exception + */ + public function update_shipping( $item, $args ) { + wc_deprecated_function( 'WC_Order::update_shipping', '3.0', 'an interaction with the WC_Order_Item_Shipping class' ); + if ( is_numeric( $item ) ) { + $item = $this->get_item( $item ); + } + if ( ! is_object( $item ) || ! $item->is_type( 'shipping' ) ) { + return false; + } + if ( ! $this->get_id() ) { + $this->save(); // Order must exist + } + + // BW compatibility for old args + if ( isset( $args['cost'] ) ) { + $args['total'] = $args['cost']; + } + + $item->set_order_id( $this->get_id() ); + $item->set_props( $args ); + $item->save(); + $this->calculate_shipping(); + + do_action( 'woocommerce_order_update_shipping', $this->get_id(), $item->get_id(), $args ); + + return $item->get_id(); + } + + /** + * Update fee for order. + * + * Note this does not update order totals. + * + * @param object|int $item + * @param array $args + * @return int updated order item ID + * @throws WC_Data_Exception + */ + public function update_fee( $item, $args ) { + wc_deprecated_function( 'WC_Order::update_fee', '3.0', 'an interaction with the WC_Order_Item_Fee class' ); + if ( is_numeric( $item ) ) { + $item = $this->get_item( $item ); + } + if ( ! is_object( $item ) || ! $item->is_type( 'fee' ) ) { + return false; + } + if ( ! $this->get_id() ) { + $this->save(); // Order must exist + } + + $item->set_order_id( $this->get_id() ); + $item->set_props( $args ); + $item->save(); + + do_action( 'woocommerce_order_update_fee', $this->get_id(), $item->get_id(), $args ); + + return $item->get_id(); + } + + /** + * Update tax line on order. + * Note this does not update order totals. + * + * @since 3.0 + * @param object|int $item + * @param array $args + * @return int updated order item ID + * @throws WC_Data_Exception + */ + public function update_tax( $item, $args ) { + wc_deprecated_function( 'WC_Order::update_tax', '3.0', 'an interaction with the WC_Order_Item_Tax class' ); + if ( is_numeric( $item ) ) { + $item = $this->get_item( $item ); + } + if ( ! is_object( $item ) || ! $item->is_type( 'tax' ) ) { + return false; + } + if ( ! $this->get_id() ) { + $this->save(); // Order must exist + } + + $item->set_order_id( $this->get_id() ); + $item->set_props( $args ); + $item->save(); + + do_action( 'woocommerce_order_update_tax', $this->get_id(), $item->get_id(), $args ); + + return $item->get_id(); + } + + /** + * Get a product (either product or variation). + * @deprecated Add deprecation notices in future release. Replaced with $item->get_product() + * @param object $item + * @return WC_Product|bool + */ + public function get_product_from_item( $item ) { + if ( is_callable( array( $item, 'get_product' ) ) ) { + $product = $item->get_product(); + } else { + $product = false; + } + return apply_filters( 'woocommerce_get_product_from_item', $product, $item, $this ); + } + + /** + * Set the customer address. + * @param array $address Address data. + * @param string $type billing or shipping. + */ + public function set_address( $address, $type = 'billing' ) { + foreach ( $address as $key => $value ) { + update_post_meta( $this->get_id(), "_{$type}_" . $key, $value ); + if ( is_callable( array( $this, "set_{$type}_{$key}" ) ) ) { + $this->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Set an order total. + * @param float $amount + * @param string $total_type + * @return bool + */ + public function legacy_set_total( $amount, $total_type = 'total' ) { + if ( ! in_array( $total_type, array( 'shipping', 'tax', 'shipping_tax', 'total', 'cart_discount', 'cart_discount_tax' ) ) ) { + return false; + } + + switch ( $total_type ) { + case 'total' : + $amount = wc_format_decimal( $amount, wc_get_price_decimals() ); + $this->set_total( $amount ); + update_post_meta( $this->get_id(), '_order_total', $amount ); + break; + case 'cart_discount' : + $amount = wc_format_decimal( $amount ); + $this->set_discount_total( $amount ); + update_post_meta( $this->get_id(), '_cart_discount', $amount ); + break; + case 'cart_discount_tax' : + $amount = wc_format_decimal( $amount ); + $this->set_discount_tax( $amount ); + update_post_meta( $this->get_id(), '_cart_discount_tax', $amount ); + break; + case 'shipping' : + $amount = wc_format_decimal( $amount ); + $this->set_shipping_total( $amount ); + update_post_meta( $this->get_id(), '_order_shipping', $amount ); + break; + case 'shipping_tax' : + $amount = wc_format_decimal( $amount ); + $this->set_shipping_tax( $amount ); + update_post_meta( $this->get_id(), '_order_shipping_tax', $amount ); + break; + case 'tax' : + $amount = wc_format_decimal( $amount ); + $this->set_cart_tax( $amount ); + update_post_meta( $this->get_id(), '_order_tax', $amount ); + break; + } + + return true; + } + + /** + * Magic __isset method for backwards compatibility. Handles legacy properties which could be accessed directly in the past. + * + * @param string $key + * @return bool + */ + public function __isset( $key ) { + $legacy_props = array( 'completed_date', 'id', 'order_type', 'post', 'status', 'post_status', 'customer_note', 'customer_message', 'user_id', 'customer_user', 'prices_include_tax', 'tax_display_cart', 'display_totals_ex_tax', 'display_cart_ex_tax', 'order_date', 'modified_date', 'cart_discount', 'cart_discount_tax', 'order_shipping', 'order_shipping_tax', 'order_total', 'order_tax', 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_state', 'billing_postcode', 'billing_country', 'billing_phone', 'billing_email', 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_address_1', 'shipping_address_2', 'shipping_city', 'shipping_state', 'shipping_postcode', 'shipping_country', 'customer_ip_address', 'customer_user_agent', 'payment_method_title', 'payment_method', 'order_currency' ); + return $this->get_id() ? ( in_array( $key, $legacy_props ) || metadata_exists( 'post', $this->get_id(), '_' . $key ) ) : false; + } + + /** + * Magic __get method for backwards compatibility. + * + * @param string $key + * @return mixed + */ + public function __get( $key ) { + wc_doing_it_wrong( $key, 'Order properties should not be accessed directly.', '3.0' ); + + if ( 'completed_date' === $key ) { + return $this->get_date_completed() ? gmdate( 'Y-m-d H:i:s', $this->get_date_completed()->getOffsetTimestamp() ) : ''; + } elseif ( 'paid_date' === $key ) { + return $this->get_date_paid() ? gmdate( 'Y-m-d H:i:s', $this->get_date_paid()->getOffsetTimestamp() ) : ''; + } elseif ( 'modified_date' === $key ) { + return $this->get_date_modified() ? gmdate( 'Y-m-d H:i:s', $this->get_date_modified()->getOffsetTimestamp() ) : ''; + } elseif ( 'order_date' === $key ) { + return $this->get_date_created() ? gmdate( 'Y-m-d H:i:s', $this->get_date_created()->getOffsetTimestamp() ) : ''; + } elseif ( 'id' === $key ) { + return $this->get_id(); + } elseif ( 'post' === $key ) { + return get_post( $this->get_id() ); + } elseif ( 'status' === $key ) { + return $this->get_status(); + } elseif ( 'post_status' === $key ) { + return get_post_status( $this->get_id() ); + } elseif ( 'customer_message' === $key || 'customer_note' === $key ) { + return $this->get_customer_note(); + } elseif ( in_array( $key, array( 'user_id', 'customer_user' ) ) ) { + return $this->get_customer_id(); + } elseif ( 'tax_display_cart' === $key ) { + return get_option( 'woocommerce_tax_display_cart' ); + } elseif ( 'display_totals_ex_tax' === $key ) { + return 'excl' === get_option( 'woocommerce_tax_display_cart' ); + } elseif ( 'display_cart_ex_tax' === $key ) { + return 'excl' === get_option( 'woocommerce_tax_display_cart' ); + } elseif ( 'cart_discount' === $key ) { + return $this->get_total_discount(); + } elseif ( 'cart_discount_tax' === $key ) { + return $this->get_discount_tax(); + } elseif ( 'order_tax' === $key ) { + return $this->get_cart_tax(); + } elseif ( 'order_shipping_tax' === $key ) { + return $this->get_shipping_tax(); + } elseif ( 'order_shipping' === $key ) { + return $this->get_shipping_total(); + } elseif ( 'order_total' === $key ) { + return $this->get_total(); + } elseif ( 'order_type' === $key ) { + return $this->get_type(); + } elseif ( 'order_currency' === $key ) { + return $this->get_currency(); + } elseif ( 'order_version' === $key ) { + return $this->get_version(); + } elseif ( is_callable( array( $this, "get_{$key}" ) ) ) { + return $this->{"get_{$key}"}(); + } else { + return get_post_meta( $this->get_id(), '_' . $key, true ); + } + } + + /** + * has_meta function for order items. This is different to the WC_Data + * version and should be removed in future versions. + * + * @deprecated + * + * @param int $order_item_id + * + * @return array of meta data. + */ + public function has_meta( $order_item_id ) { + global $wpdb; + + wc_deprecated_function( 'WC_Order::has_meta( $order_item_id )', '3.0', 'WC_Order_item::get_meta_data' ); + + return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value, meta_id, order_item_id + FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d + ORDER BY meta_id", absint( $order_item_id ) ), ARRAY_A ); + } + + /** + * Display meta data belonging to an item. + * @param array $item + */ + public function display_item_meta( $item ) { + wc_deprecated_function( 'WC_Order::display_item_meta', '3.0', 'wc_display_item_meta' ); + $product = $item->get_product(); + $item_meta = new WC_Order_Item_Meta( $item, $product ); + $item_meta->display(); + } + + /** + * Display download links for an order item. + * @param array $item + */ + public function display_item_downloads( $item ) { + wc_deprecated_function( 'WC_Order::display_item_downloads', '3.0', 'wc_display_item_downloads' ); + $product = $item->get_product(); + + if ( $product && $product->exists() && $product->is_downloadable() && $this->is_download_permitted() ) { + $download_files = $this->get_item_downloads( $item ); + $i = 0; + $links = array(); + + foreach ( $download_files as $download_id => $file ) { + $i++; + /* translators: 1: current item count */ + $prefix = count( $download_files ) > 1 ? sprintf( __( 'Download %d', 'woocommerce' ), $i ) : __( 'Download', 'woocommerce' ); + $links[] = '' . $prefix . ': ' . esc_html( $file['name'] ) . '' . "\n"; + } + + echo '
    ' . implode( '
    ', $links ); + } + } + + /** + * Get the Download URL. + * + * @param int $product_id + * @param int $download_id + * @return string + */ + public function get_download_url( $product_id, $download_id ) { + wc_deprecated_function( 'WC_Order::get_download_url', '3.0', 'WC_Order_Item_Product::get_item_download_url' ); + return add_query_arg( array( + 'download_file' => $product_id, + 'order' => $this->get_order_key(), + 'email' => urlencode( $this->get_billing_email() ), + 'key' => $download_id, + ), trailingslashit( home_url() ) ); + } + + /** + * Get the downloadable files for an item in this order. + * + * @param array $item + * @return array + */ + public function get_item_downloads( $item ) { + wc_deprecated_function( 'WC_Order::get_item_downloads', '3.0', 'WC_Order_Item_Product::get_item_downloads' ); + + if ( ! $item instanceof WC_Order_Item ) { + if ( ! empty( $item['variation_id'] ) ) { + $product_id = $item['variation_id']; + } elseif ( ! empty( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } else { + return array(); + } + + // Create a 'virtual' order item to allow retrieving item downloads when + // an array of product_id is passed instead of actual order item. + $item = new WC_Order_Item_Product(); + $item->set_product( wc_get_product( $product_id ) ); + $item->set_order_id( $this->get_id() ); + } + + return $item->get_item_downloads(); + } + + /** + * Gets shipping total. Alias of WC_Order::get_shipping_total(). + * @deprecated 3.0.0 since this is an alias only. + * @return float + */ + public function get_total_shipping() { + return $this->get_shipping_total(); + } + + /** + * Get order item meta. + * @deprecated 3.0.0 + * @param mixed $order_item_id + * @param string $key (default: '') + * @param bool $single (default: false) + * @return array|string + */ + public function get_item_meta( $order_item_id, $key = '', $single = false ) { + wc_deprecated_function( 'WC_Order::get_item_meta', '3.0', 'wc_get_order_item_meta' ); + return get_metadata( 'order_item', $order_item_id, $key, $single ); + } + + /** + * Get all item meta data in array format in the order it was saved. Does not group meta by key like get_item_meta(). + * + * @param mixed $order_item_id + * @return array of objects + */ + public function get_item_meta_array( $order_item_id ) { + wc_deprecated_function( 'WC_Order::get_item_meta_array', '3.0', 'WC_Order_Item::get_meta_data() (note the format has changed)' ); + $item = $this->get_item( $order_item_id ); + $meta_data = $item->get_meta_data(); + $item_meta_array = array(); + + foreach ( $meta_data as $meta ) { + $item_meta_array[ $meta->id ] = $meta; + } + + return $item_meta_array; + } + + /** + * Expand item meta into the $item array. + * @deprecated 3.0.0 Item meta no longer expanded due to new order item + * classes. This function now does nothing to avoid data breakage. + * @param array $item before expansion. + * @return array + */ + public function expand_item_meta( $item ) { + wc_deprecated_function( 'WC_Order::expand_item_meta', '3.0' ); + return $item; + } + + /** + * Load the order object. Called from the constructor. + * @deprecated 3.0.0 Logic moved to constructor + * @param int|object|WC_Order $order Order to init. + */ + protected function init( $order ) { + wc_deprecated_function( 'WC_Order::init', '3.0', 'Logic moved to constructor' ); + if ( is_numeric( $order ) ) { + $this->set_id( $order ); + } elseif ( $order instanceof WC_Order ) { + $this->set_id( absint( $order->get_id() ) ); + } elseif ( isset( $order->ID ) ) { + $this->set_id( absint( $order->ID ) ); + } + $this->set_object_read( false ); + $this->data_store->read( $this ); + } + + /** + * Gets an order from the database. + * @deprecated 3.0 + * @param int $id (default: 0). + * @return bool + */ + public function get_order( $id = 0 ) { + wc_deprecated_function( 'WC_Order::get_order', '3.0' ); + + if ( ! $id ) { + return false; + } + + $result = get_post( $id ); + + if ( $result ) { + $this->populate( $result ); + return true; + } + + return false; + } + + /** + * Populates an order from the loaded post data. + * @deprecated 3.0 + * @param mixed $result + */ + public function populate( $result ) { + wc_deprecated_function( 'WC_Order::populate', '3.0' ); + $this->set_id( $result->ID ); + $this->set_object_read( false ); + $this->data_store->read( $this ); + } + + /** + * Cancel the order and restore the cart (before payment). + * @deprecated 3.0.0 Moved to event handler. + * @param string $note (default: '') Optional note to add. + */ + public function cancel_order( $note = '' ) { + wc_deprecated_function( 'WC_Order::cancel_order', '3.0', 'WC_Order::update_status' ); + WC()->session->set( 'order_awaiting_payment', false ); + $this->update_status( 'cancelled', $note ); + } + + /** + * Record sales. + * @deprecated 3.0.0 + */ + public function record_product_sales() { + wc_deprecated_function( 'WC_Order::record_product_sales', '3.0', 'wc_update_total_sales_counts' ); + wc_update_total_sales_counts( $this->get_id() ); + } + + /** + * Increase applied coupon counts. + * @deprecated 3.0.0 + */ + public function increase_coupon_usage_counts() { + wc_deprecated_function( 'WC_Order::increase_coupon_usage_counts', '3.0', 'wc_update_coupon_usage_counts' ); + wc_update_coupon_usage_counts( $this->get_id() ); + } + + /** + * Decrease applied coupon counts. + * @deprecated 3.0.0 + */ + public function decrease_coupon_usage_counts() { + wc_deprecated_function( 'WC_Order::decrease_coupon_usage_counts', '3.0', 'wc_update_coupon_usage_counts' ); + wc_update_coupon_usage_counts( $this->get_id() ); + } + + /** + * Reduce stock levels for all line items in the order. + * @deprecated 3.0.0 + */ + public function reduce_order_stock() { + wc_deprecated_function( 'WC_Order::reduce_order_stock', '3.0', 'wc_reduce_stock_levels' ); + wc_reduce_stock_levels( $this->get_id() ); + } + + /** + * Send the stock notifications. + * @deprecated 3.0.0 No longer needs to be called directly. + * + * @param $product + * @param $new_stock + * @param $qty_ordered + */ + public function send_stock_notifications( $product, $new_stock, $qty_ordered ) { + wc_deprecated_function( 'WC_Order::send_stock_notifications', '3.0' ); + } + + /** + * Output items for display in html emails. + * @deprecated 3.0.0 Moved to template functions. + * @param array $args Items args. + * @return string + */ + public function email_order_items_table( $args = array() ) { + wc_deprecated_function( 'WC_Order::email_order_items_table', '3.0', 'wc_get_email_order_items' ); + return wc_get_email_order_items( $this, $args ); + } + + /** + * Get currency. + * @deprecated 3.0.0 + */ + public function get_order_currency() { + wc_deprecated_function( 'WC_Order::get_order_currency', '3.0', 'WC_Order::get_currency' ); + return apply_filters( 'woocommerce_get_order_currency', $this->get_currency(), $this ); + } +} diff --git a/includes/legacy/abstract-wc-legacy-payment-token.php b/includes/legacy/abstract-wc-legacy-payment-token.php new file mode 100644 index 00000000000..d1316d1b4cd --- /dev/null +++ b/includes/legacy/abstract-wc-legacy-payment-token.php @@ -0,0 +1,70 @@ +read, ->update or ->create + * directly on the object. + * + * @version 3.0.0 + * @package WooCommerce/Classes + * @category Class + * @author WooCommerce + */ +abstract class WC_Legacy_Payment_Token extends WC_Data { + + /** + * Sets the type of this payment token (CC, eCheck, or something else). + * + * @param string Payment Token Type (CC, eCheck) + */ + public function set_type( $type ) { + wc_deprecated_function( 'WC_Payment_Token::set_type', '3.0.0', 'Type cannot be overwritten.' ); + } + + /** + * Read a token by ID. + * @deprecated 3.0.0 - Init a token class with an ID. + * + * @param int $token_id + */ + public function read( $token_id ) { + wc_deprecated_function( 'WC_Payment_Token::read', '3.0.0', 'a new token class initialized with an ID.' ); + $this->set_id( $token_id ); + $data_store = WC_Data_Store::load( 'payment-token' ); + $data_store->read( $this ); + } + + /** + * Update a token. + * @deprecated 3.0.0 - Use ::save instead. + */ + public function update() { + wc_deprecated_function( 'WC_Payment_Token::update', '3.0.0', 'WC_Payment_Token::save instead.' ); + $data_store = WC_Data_Store::load( 'payment-token' ); + try { + $data_store->update( $this ); + } catch ( Exception $e ) { + return false; + } + } + + /** + * Create a token. + * @deprecated 3.0.0 - Use ::save instead. + */ + public function create() { + wc_deprecated_function( 'WC_Payment_Token::create', '3.0.0', 'WC_Payment_Token::save instead.' ); + $data_store = WC_Data_Store::load( 'payment-token' ); + try { + $data_store->create( $this ); + } catch ( Exception $e ) { + return false; + } + } + +} diff --git a/includes/legacy/abstract-wc-legacy-product.php b/includes/legacy/abstract-wc-legacy-product.php new file mode 100644 index 00000000000..577989f4d31 --- /dev/null +++ b/includes/legacy/abstract-wc-legacy-product.php @@ -0,0 +1,689 @@ +is_type( 'variation' ) ) { + $valid = array_merge( $valid, array( + 'variation_id', + 'variation_data', + 'variation_has_stock', + 'variation_shipping_class_id', + 'variation_has_sku', + 'variation_has_length', + 'variation_has_width', + 'variation_has_height', + 'variation_has_weight', + 'variation_has_tax_class', + 'variation_has_downloadable_files', + ) ); + } + return in_array( $key, array_merge( $valid, array_keys( $this->data ) ) ) || metadata_exists( 'post', $this->get_id(), '_' . $key ) || metadata_exists( 'post', $this->get_parent_id(), '_' . $key ); + } + + /** + * Magic __get method for backwards compatibility. Maps legacy vars to new getters. + * + * @param string $key Key name. + * @return mixed + */ + public function __get( $key ) { + + if ( 'post_type' === $key ) { + return $this->post_type; + } + + wc_doing_it_wrong( $key, __( 'Product properties should not be accessed directly.', 'woocommerce' ), '3.0' ); + + switch ( $key ) { + case 'id' : + $value = $this->is_type( 'variation' ) ? $this->get_parent_id() : $this->get_id(); + break; + case 'product_type' : + $value = $this->get_type(); + break; + case 'product_attributes' : + $value = isset( $this->data['attributes'] ) ? $this->data['attributes'] : ''; + break; + case 'visibility' : + $value = $this->get_catalog_visibility(); + break; + case 'sale_price_dates_from' : + return $this->get_date_on_sale_from() ? $this->get_date_on_sale_from()->getTimestamp() : ''; + break; + case 'sale_price_dates_to' : + return $this->get_date_on_sale_to() ? $this->get_date_on_sale_to()->getTimestamp() : ''; + break; + case 'post' : + $value = get_post( $this->get_id() ); + break; + case 'download_type' : + return 'standard'; + break; + case 'product_image_gallery' : + $value = $this->get_gallery_image_ids(); + break; + case 'variation_shipping_class' : + case 'shipping_class' : + $value = $this->get_shipping_class(); + break; + case 'total_stock' : + $value = $this->get_total_stock(); + break; + case 'downloadable' : + case 'virtual' : + case 'manage_stock' : + case 'featured' : + case 'sold_individually' : + $value = $this->{"get_$key"}() ? 'yes' : 'no'; + break; + case 'crosssell_ids' : + $value = $this->get_cross_sell_ids(); + break; + case 'upsell_ids' : + $value = $this->get_upsell_ids(); + break; + case 'parent' : + $value = wc_get_product( $this->get_parent_id() ); + break; + case 'variation_id' : + $value = $this->is_type( 'variation' ) ? $this->get_id() : ''; + break; + case 'variation_data' : + $value = $this->is_type( 'variation' ) ? wc_get_product_variation_attributes( $this->get_id() ) : ''; + break; + case 'variation_has_stock' : + $value = $this->is_type( 'variation' ) ? $this->managing_stock() : ''; + break; + case 'variation_shipping_class_id' : + $value = $this->is_type( 'variation' ) ? $this->get_shipping_class_id() : ''; + break; + case 'variation_has_sku' : + case 'variation_has_length' : + case 'variation_has_width' : + case 'variation_has_height' : + case 'variation_has_weight' : + case 'variation_has_tax_class' : + case 'variation_has_downloadable_files' : + $value = true; // These were deprecated in 2.2 and simply returned true in 2.6.x. + break; + default : + if ( in_array( $key, array_keys( $this->data ) ) ) { + $value = $this->{"get_$key"}(); + } else { + $value = get_post_meta( $this->id, '_' . $key, true ); + } + break; + } + return $value; + } + + /** + * If set, get the default attributes for a variable product. + * + * @deprecated 3.0.0 + * @return array + */ + public function get_variation_default_attributes() { + wc_deprecated_function( 'WC_Product_Variable::get_variation_default_attributes', '3.0', 'WC_Product::get_default_attributes' ); + return apply_filters( 'woocommerce_product_default_attributes', $this->get_default_attributes(), $this ); + } + + /** + * Returns the gallery attachment ids. + * + * @deprecated 3.0.0 + * @return array + */ + public function get_gallery_attachment_ids() { + wc_deprecated_function( 'WC_Product::get_gallery_attachment_ids', '3.0', 'WC_Product::get_gallery_image_ids' ); + return $this->get_gallery_image_ids(); + } + + /** + * Set stock level of the product. + * + * @deprecated 3.0.0 + * + * @param int $amount + * @param string $mode + * + * @return int + */ + public function set_stock( $amount = null, $mode = 'set' ) { + wc_deprecated_function( 'WC_Product::set_stock', '3.0', 'wc_update_product_stock' ); + return wc_update_product_stock( $this, $amount, $mode ); + } + + /** + * Reduce stock level of the product. + * + * @deprecated 3.0.0 + * @param int $amount Amount to reduce by. Default: 1 + * @return int new stock level + */ + public function reduce_stock( $amount = 1 ) { + wc_deprecated_function( 'WC_Product::reduce_stock', '3.0', 'wc_update_product_stock' ); + return wc_update_product_stock( $this, $amount, 'decrease' ); + } + + /** + * Increase stock level of the product. + * + * @deprecated 3.0.0 + * @param int $amount Amount to increase by. Default 1. + * @return int new stock level + */ + public function increase_stock( $amount = 1 ) { + wc_deprecated_function( 'WC_Product::increase_stock', '3.0', 'wc_update_product_stock' ); + return wc_update_product_stock( $this, $amount, 'increase' ); + } + + /** + * Check if the stock status needs changing. + * + * @deprecated 3.0.0 Sync is done automatically on read/save, so calling this should not be needed any more. + */ + public function check_stock_status() { + wc_deprecated_function( 'WC_Product::check_stock_status', '3.0' ); + } + + /** + * Get and return related products. + * @deprecated 3.0.0 Use wc_get_related_products instead. + * + * @param int $limit + * + * @return array + */ + public function get_related( $limit = 5 ) { + wc_deprecated_function( 'WC_Product::get_related', '3.0', 'wc_get_related_products' ); + return wc_get_related_products( $this->get_id(), $limit ); + } + + /** + * Retrieves related product terms. + * @deprecated 3.0.0 Use wc_get_product_term_ids instead. + * + * @param $term + * + * @return array + */ + protected function get_related_terms( $term ) { + wc_deprecated_function( 'WC_Product::get_related_terms', '3.0', 'wc_get_product_term_ids' ); + return array_merge( array( 0 ), wc_get_product_term_ids( $this->get_id(), $term ) ); + } + + /** + * Builds the related posts query. + * @deprecated 3.0.0 Use Product Data Store get_related_products_query instead. + * + * @param $cats_array + * @param $tags_array + * @param $exclude_ids + * @param $limit + */ + protected function build_related_query( $cats_array, $tags_array, $exclude_ids, $limit ) { + wc_deprecated_function( 'WC_Product::build_related_query', '3.0', 'Product Data Store get_related_products_query' ); + $data_store = WC_Data_Store::load( 'product' ); + return $data_store->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ); + } + + /** + * Returns the child product. + * @deprecated 3.0.0 Use wc_get_product instead. + * @param mixed $child_id + * @return WC_Product|WC_Product|WC_Product_variation + */ + public function get_child( $child_id ) { + wc_deprecated_function( 'WC_Product::get_child', '3.0', 'wc_get_product' ); + return wc_get_product( $child_id ); + } + + /** + * Functions for getting parts of a price, in html, used by get_price_html. + * + * @deprecated 3.0.0 + * @return string + */ + public function get_price_html_from_text() { + wc_deprecated_function( 'WC_Product::get_price_html_from_text', '3.0', 'wc_get_price_html_from_text' ); + return wc_get_price_html_from_text(); + } + + /** + * Functions for getting parts of a price, in html, used by get_price_html. + * + * @deprecated 3.0.0 Use wc_format_sale_price instead. + * @param string $from String or float to wrap with 'from' text + * @param mixed $to String or float to wrap with 'to' text + * @return string + */ + public function get_price_html_from_to( $from, $to ) { + wc_deprecated_function( 'WC_Product::get_price_html_from_to', '3.0', 'wc_format_sale_price' ); + return apply_filters( 'woocommerce_get_price_html_from_to', wc_format_sale_price( $from, $to ), $from, $to, $this ); + } + + /** + * Lists a table of attributes for the product page. + * @deprecated 3.0.0 Use wc_display_product_attributes instead. + */ + public function list_attributes() { + wc_deprecated_function( 'WC_Product::list_attributes', '3.0', 'wc_display_product_attributes' ); + wc_display_product_attributes( $this ); + } + + /** + * Returns the price (including tax). Uses customer tax rates. Can work for a specific $qty for more accurate taxes. + * + * @deprecated 3.0.0 Use wc_get_price_including_tax instead. + * @param int $qty + * @param string $price to calculate, left blank to just use get_price() + * @return string + */ + public function get_price_including_tax( $qty = 1, $price = '' ) { + wc_deprecated_function( 'WC_Product::get_price_including_tax', '3.0', 'wc_get_price_including_tax' ); + return wc_get_price_including_tax( $this, array( 'qty' => $qty, 'price' => $price ) ); + } + + /** + * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting. + * + * @deprecated 3.0.0 Use wc_get_price_to_display instead. + * @param string $price to calculate, left blank to just use get_price() + * @param integer $qty passed on to get_price_including_tax() or get_price_excluding_tax() + * @return string + */ + public function get_display_price( $price = '', $qty = 1 ) { + wc_deprecated_function( 'WC_Product::get_display_price', '3.0', 'wc_get_price_to_display' ); + return wc_get_price_to_display( $this, array( 'qty' => $qty, 'price' => $price ) ); + } + + /** + * Returns the price (excluding tax) - ignores tax_class filters since the price may *include* tax and thus needs subtracting. + * Uses store base tax rates. Can work for a specific $qty for more accurate taxes. + * + * @deprecated 3.0.0 Use wc_get_price_excluding_tax instead. + * @param int $qty + * @param string $price to calculate, left blank to just use get_price() + * @return string + */ + public function get_price_excluding_tax( $qty = 1, $price = '' ) { + wc_deprecated_function( 'WC_Product::get_price_excluding_tax', '3.0', 'wc_get_price_excluding_tax' ); + return wc_get_price_excluding_tax( $this, array( 'qty' => $qty, 'price' => $price ) ); + } + + /** + * Adjust a products price dynamically. + * + * @deprecated 3.0.0 + * @param mixed $price + */ + public function adjust_price( $price ) { + wc_deprecated_function( 'WC_Product::adjust_price', '3.0', 'WC_Product::set_price / WC_Product::get_price' ); + $this->data['price'] = $this->data['price'] + $price; + } + + /** + * Returns the product categories. + * + * @deprecated 3.0.0 + * @param string $sep (default: ', '). + * @param string $before (default: ''). + * @param string $after (default: ''). + * @return string + */ + public function get_categories( $sep = ', ', $before = '', $after = '' ) { + wc_deprecated_function( 'WC_Product::get_categories', '3.0', 'wc_get_product_category_list' ); + return wc_get_product_category_list( $this->get_id(), $sep, $before, $after ); + } + + /** + * Returns the product tags. + * + * @deprecated 3.0.0 + * @param string $sep (default: ', '). + * @param string $before (default: ''). + * @param string $after (default: ''). + * @return array + */ + public function get_tags( $sep = ', ', $before = '', $after = '' ) { + wc_deprecated_function( 'WC_Product::get_tags', '3.0', 'wc_get_product_tag_list' ); + return wc_get_product_tag_list( $this->get_id(), $sep, $before, $after ); + } + + /** + * Get the product's post data. + * + * @deprecated 3.0.0 + * @return WP_Post + */ + public function get_post_data() { + wc_deprecated_function( 'WC_Product::get_post_data', '3.0', 'get_post' ); + + // In order to keep backwards compatibility it's required to use the parent data for variations. + if ( $this->is_type( 'variation' ) ) { + $post_data = get_post( $this->get_parent_id() ); + } else { + $post_data = get_post( $this->get_id() ); + } + + return $post_data; + } + + /** + * Get the parent of the post. + * + * @deprecated 3.0.0 + * @return int + */ + public function get_parent() { + wc_deprecated_function( 'WC_Product::get_parent', '3.0', 'WC_Product::get_parent_id' ); + return apply_filters( 'woocommerce_product_parent', absint( $this->get_post_data()->post_parent ), $this ); + } + + /** + * Returns the upsell product ids. + * + * @deprecated 3.0.0 + * @return array + */ + public function get_upsells() { + wc_deprecated_function( 'WC_Product::get_upsells', '3.0', 'WC_Product::get_upsell_ids' ); + return apply_filters( 'woocommerce_product_upsell_ids', $this->get_upsell_ids(), $this ); + } + + /** + * Returns the cross sell product ids. + * + * @deprecated 3.0.0 + * @return array + */ + public function get_cross_sells() { + wc_deprecated_function( 'WC_Product::get_cross_sells', '3.0', 'WC_Product::get_cross_sell_ids' ); + return apply_filters( 'woocommerce_product_crosssell_ids', $this->get_cross_sell_ids(), $this ); + } + + /** + * Check if variable product has default attributes set. + * + * @deprecated 3.0.0 + * @return bool + */ + public function has_default_attributes() { + wc_deprecated_function( 'WC_Product_Variable::has_default_attributes', '3.0', 'a check against WC_Product::get_default_attributes directly' ); + if ( ! $this->get_default_attributes() ) { + return true; + } + return false; + } + + /** + * Get variation ID. + * + * @deprecated 3.0.0 + * @return int + */ + public function get_variation_id() { + wc_deprecated_function( 'WC_Product::get_variation_id', '3.0', 'WC_Product::get_id(). It will always be the variation ID if this is a variation.' ); + return $this->get_id(); + } + + /** + * Get product variation description. + * + * @deprecated 3.0.0 + * @return string + */ + public function get_variation_description() { + wc_deprecated_function( 'WC_Product::get_variation_description', '3.0', 'WC_Product::get_description()' ); + return $this->get_description(); + } + + /** + * Check if all variation's attributes are set. + * + * @deprecated 3.0.0 + * @return boolean + */ + public function has_all_attributes_set() { + wc_deprecated_function( 'WC_Product::has_all_attributes_set', '3.0', 'an array filter on get_variation_attributes for a quick solution.' ); + $set = true; + + // undefined attributes have null strings as array values + foreach ( $this->get_variation_attributes() as $att ) { + if ( ! $att ) { + $set = false; + break; + } + } + return $set; + } + + /** + * Returns whether or not the variations parent is visible. + * + * @deprecated 3.0.0 + * @return bool + */ + public function parent_is_visible() { + wc_deprecated_function( 'WC_Product::parent_is_visible', '3.0' ); + return $this->is_visible(); + } + + /** + * Get total stock - This is the stock of parent and children combined. + * + * @deprecated 3.0.0 + * @return int + */ + public function get_total_stock() { + wc_deprecated_function( 'WC_Product::get_total_stock', '3.0', 'get_stock_quantity on each child. Beware of performance issues in doing so.' ); + if ( sizeof( $this->get_children() ) > 0 ) { + $total_stock = max( 0, $this->get_stock_quantity() ); + + foreach ( $this->get_children() as $child_id ) { + if ( 'yes' === get_post_meta( $child_id, '_manage_stock', true ) ) { + $stock = get_post_meta( $child_id, '_stock', true ); + $total_stock += max( 0, wc_stock_amount( $stock ) ); + } + } + } else { + $total_stock = $this->get_stock_quantity(); + } + return wc_stock_amount( $total_stock ); + } + + /** + * Get formatted variation data with WC < 2.4 back compat and proper formatting of text-based attribute names. + * + * @deprecated 3.0.0 + * + * @param bool $flat + * + * @return string + */ + public function get_formatted_variation_attributes( $flat = false ) { + wc_deprecated_function( 'WC_Product::get_formatted_variation_attributes', '3.0', 'wc_get_formatted_variation' ); + return wc_get_formatted_variation( $this, $flat ); + } + + /** + * Sync variable product prices with the children lowest/highest prices. + * + * @deprecated 3.0.0 not used in core. + * + * @param int $product_id + */ + public function variable_product_sync( $product_id = 0 ) { + wc_deprecated_function( 'WC_Product::variable_product_sync', '3.0' ); + if ( empty( $product_id ) ) { + $product_id = $this->get_id(); + } + + // Sync prices with children + if ( is_callable( array( __CLASS__, 'sync' ) ) ) { + self::sync( $product_id ); + } + } + + /** + * Sync the variable product's attributes with the variations. + * + * @param $product + * @param bool $children + */ + public static function sync_attributes( $product, $children = false ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + + /** + * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. + * Attempt to get full version of the text attribute from the parent and UPDATE meta. + */ + if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { + $parent_attributes = array_filter( (array) get_post_meta( $product->get_id(), '_product_attributes', true ) ); + + if ( ! $children ) { + $children = $product->get_children( 'edit' ); + } + + foreach ( $children as $child_id ) { + $all_meta = get_post_meta( $child_id ); + + foreach ( $all_meta as $name => $value ) { + if ( 0 !== strpos( $name, 'attribute_' ) ) { + continue; + } + if ( sanitize_title( $value[0] ) === $value[0] ) { + foreach ( $parent_attributes as $attribute ) { + if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) { + continue; + } + $text_attributes = wc_get_text_attributes( $attribute['value'] ); + foreach ( $text_attributes as $text_attribute ) { + if ( sanitize_title( $text_attribute ) === $value[0] ) { + update_post_meta( $child_id, $name, $text_attribute ); + break; + } + } + } + } + } + } + } + } + + /** + * Match a variation to a given set of attributes using a WP_Query. + * @deprecated 3.0.0 in favour of Product data store's find_matching_product_variation. + * + * @param array $match_attributes + */ + public function get_matching_variation( $match_attributes = array() ) { + wc_deprecated_function( 'WC_Product::get_matching_variation', '3.0', 'Product data store find_matching_product_variation' ); + $data_store = WC_Data_Store::load( 'product' ); + return $data_store->find_matching_product_variation( $this, $match_attributes ); + } + + /** + * Returns whether or not we are showing dimensions on the product page. + * @deprecated 3.0.0 Unused. + * @return bool + */ + public function enable_dimensions_display() { + wc_deprecated_function( 'WC_Product::enable_dimensions_display', '3.0' ); + return apply_filters( 'wc_product_enable_dimensions_display', true ) && ( $this->has_dimensions() || $this->has_weight() || $this->child_has_weight() || $this->child_has_dimensions() ); + } + + /** + * Returns the product rating in html format. + * + * @deprecated 3.0.0 + * @param string $rating (default: '') + * @return string + */ + public function get_rating_html( $rating = null ) { + wc_deprecated_function( 'WC_Product::get_rating_html', '3.0', 'wc_get_rating_html' ); + return wc_get_rating_html( $rating ); + } + + /** + * Sync product rating. Can be called statically. + * + * @deprecated 3.0.0 + * @param int $post_id + */ + public static function sync_average_rating( $post_id ) { + wc_deprecated_function( 'WC_Product::sync_average_rating', '3.0', 'WC_Comments::get_average_rating_for_product or leave to CRUD.' ); + $average = WC_Comments::get_average_rating_for_product( wc_get_product( $post_id ) ); + update_post_meta( $post_id, '_wc_average_rating', $average ); + } + + /** + * Sync product rating count. Can be called statically. + * + * @deprecated 3.0.0 + * @param int $post_id + */ + public static function sync_rating_count( $post_id ) { + wc_deprecated_function( 'WC_Product::sync_rating_count', '3.0', 'WC_Comments::get_rating_counts_for_product or leave to CRUD.' ); + $counts = WC_Comments::get_rating_counts_for_product( wc_get_product( $post_id ) ); + update_post_meta( $post_id, '_wc_rating_count', $counts ); + } + + /** + * Same as get_downloads in CRUD. + * + * @deprecated 3.0.0 + * @return array + */ + public function get_files() { + wc_deprecated_function( 'WC_Product::get_files', '3.0', 'WC_Product::get_downloads' ); + return $this->get_downloads(); + } + + /** + * @deprected 3.0.0 Sync is taken care of during save - no need to call this directly. + */ + public function grouped_product_sync() { + wc_deprecated_function( 'WC_Product::grouped_product_sync', '3.0' ); + } +} diff --git a/includes/legacy/class-wc-legacy-coupon.php b/includes/legacy/class-wc-legacy-coupon.php new file mode 100644 index 00000000000..91c5fd893ee --- /dev/null +++ b/includes/legacy/class-wc-legacy-coupon.php @@ -0,0 +1,204 @@ +get_id(); + break; + case 'exists' : + $value = ( $this->get_id() > 0 ) ? true : false; + break; + case 'coupon_custom_fields' : + $legacy_custom_fields = array(); + $custom_fields = $this->get_id() ? $this->get_meta_data() : array(); + if ( ! empty( $custom_fields ) ) { + foreach ( $custom_fields as $cf_value ) { + // legacy only supports 1 key + $legacy_custom_fields[ $cf_value->key ][0] = $cf_value->value; + } + } + $value = $legacy_custom_fields; + break; + case 'type' : + case 'discount_type' : + $value = $this->get_discount_type(); + break; + case 'amount' : + case 'coupon_amount' : + $value = $this->get_amount(); + break; + case 'code' : + $value = $this->get_code(); + break; + case 'individual_use' : + $value = ( true === $this->get_individual_use() ) ? 'yes' : 'no'; + break; + case 'product_ids' : + $value = $this->get_product_ids(); + break; + case 'exclude_product_ids' : + $value = $this->get_excluded_product_ids(); + break; + case 'usage_limit' : + $value = $this->get_usage_limit(); + break; + case 'usage_limit_per_user' : + $value = $this->get_usage_limit_per_user(); + break; + case 'limit_usage_to_x_items' : + $value = $this->get_limit_usage_to_x_items(); + break; + case 'usage_count' : + $value = $this->get_usage_count(); + break; + case 'expiry_date' : + $value = ( $this->get_date_expires() ? $this->get_date_expires()->date( 'Y-m-d' ) : '' ); + break; + case 'product_categories' : + $value = $this->get_product_categories(); + break; + case 'exclude_product_categories' : + $value = $this->get_excluded_product_categories(); + break; + case 'minimum_amount' : + $value = $this->get_minimum_amount(); + break; + case 'maximum_amount' : + $value = $this->get_maximum_amount(); + break; + case 'customer_email' : + $value = $this->get_email_restrictions(); + break; + default : + $value = ''; + break; + } + + return $value; + } + + /** + * Format loaded data as array. + * @param string|array $array + * @return array + */ + public function format_array( $array ) { + wc_deprecated_function( 'WC_Coupon::format_array', '3.0' ); + if ( ! is_array( $array ) ) { + if ( is_serialized( $array ) ) { + $array = maybe_unserialize( $array ); + } else { + $array = explode( ',', $array ); + } + } + return array_filter( array_map( 'trim', array_map( 'strtolower', $array ) ) ); + } + + + /** + * Check if coupon needs applying before tax. + * + * @return bool + */ + public function apply_before_tax() { + wc_deprecated_function( 'WC_Coupon::apply_before_tax', '3.0' ); + return true; + } + + /** + * Check if a coupon enables free shipping. + * + * @return bool + */ + public function enable_free_shipping() { + wc_deprecated_function( 'WC_Coupon::enable_free_shipping', '3.0', 'WC_Coupon::get_free_shipping' ); + return $this->get_free_shipping(); + } + + /** + * Check if a coupon excludes sale items. + * + * @return bool + */ + public function exclude_sale_items() { + wc_deprecated_function( 'WC_Coupon::exclude_sale_items', '3.0', 'WC_Coupon::get_exclude_sale_items' ); + return $this->get_exclude_sale_items(); + } + + /** + * Increase usage count for current coupon. + * + * @param string $used_by Either user ID or billing email + */ + public function inc_usage_count( $used_by = '' ) { + $this->increase_usage_count( $used_by ); + } + + /** + * Decrease usage count for current coupon. + * + * @param string $used_by Either user ID or billing email + */ + public function dcr_usage_count( $used_by = '' ) { + $this->decrease_usage_count( $used_by ); + } +} diff --git a/includes/legacy/class-wc-legacy-customer.php b/includes/legacy/class-wc-legacy-customer.php new file mode 100644 index 00000000000..5ad3d952afe --- /dev/null +++ b/includes/legacy/class-wc-legacy-customer.php @@ -0,0 +1,286 @@ +filter_legacy_key( $key ); + return in_array( $key, $legacy_keys ); + } + + /** + * __get function. + * @param string $key + * @return string + */ + public function __get( $key ) { + wc_doing_it_wrong( $key, 'Customer properties should not be accessed directly.', '3.0' ); + $key = $this->filter_legacy_key( $key ); + if ( in_array( $key, array( 'country', 'state', 'postcode', 'city', 'address_1', 'address', 'address_2' ) ) ) { + $key = 'billing_' . $key; + } + return is_callable( array( $this, "get_{$key}" ) ) ? $this->{"get_{$key}"}() : ''; + } + + /** + * __set function. + * + * @param string $key + * @param mixed $value + */ + public function __set( $key, $value ) { + wc_doing_it_wrong( $key, 'Customer properties should not be set directly.', '3.0' ); + $key = $this->filter_legacy_key( $key ); + + if ( is_callable( array( $this, "set_{$key}" ) ) ) { + $this->{"set_{$key}"}( $value ); + } + } + + /** + * Address and shipping_address are aliased, so we want to get the 'real' key name. + * For all other keys, we can just return it. + * @since 3.0.0 + * @param string $key + * @return string + */ + private function filter_legacy_key( $key ) { + if ( 'address' === $key ) { + $key = 'address_1'; + } + if ( 'shipping_address' === $key ) { + $key = 'shipping_address_1'; + } + + return $key; + } + + /** + * Sets session data for the location. + * + * @param string $country + * @param string $state + * @param string $postcode (default: '') + * @param string $city (default: '') + */ + public function set_location( $country, $state, $postcode = '', $city = '' ) { + $this->set_billing_location( $country, $state, $postcode, $city ); + $this->set_shipping_location( $country, $state, $postcode, $city ); + } + + /** + * Get default country for a customer. + * @return string + */ + public function get_default_country() { + wc_deprecated_function( 'WC_Customer::get_default_country', '3.0', 'wc_get_customer_default_location' ); + $default = wc_get_customer_default_location(); + return $default['country']; + } + + /** + * Get default state for a customer. + * @return string + */ + public function get_default_state() { + wc_deprecated_function( 'WC_Customer::get_default_state', '3.0', 'wc_get_customer_default_location' ); + $default = wc_get_customer_default_location(); + return $default['state']; + } + + /** + * Set customer address to match shop base address. + */ + public function set_to_base() { + wc_deprecated_function( 'WC_Customer::set_to_base', '3.0', 'WC_Customer::set_billing_address_to_base' ); + $this->set_billing_address_to_base(); + } + + /** + * Set customer shipping address to base address. + */ + public function set_shipping_to_base() { + wc_deprecated_function( 'WC_Customer::set_shipping_to_base', '3.0', 'WC_Customer::set_shipping_address_to_base' ); + $this->set_shipping_address_to_base(); + } + + /** + * Calculated shipping. + * @param boolean $calculated + */ + public function calculated_shipping( $calculated = true ) { + wc_deprecated_function( 'WC_Customer::calculated_shipping', '3.0', 'WC_Customer::set_calculated_shipping' ); + $this->set_calculated_shipping( $calculated ); + } + + /** + * Set default data for a customer. + */ + public function set_default_data() { + wc_deprecated_function( 'WC_Customer::set_default_data', '3.0' ); + } + + /** + * Save data function. + */ + public function save_data() { + $this->save(); + } + + /** + * Is the user a paying customer? + * + * @param int $user_id + * + * @return bool + */ + function is_paying_customer( $user_id = '' ) { + wc_deprecated_function( 'WC_Customer::is_paying_customer', '3.0', 'WC_Customer::get_is_paying_customer' ); + if ( ! empty( $user_id ) ) { + $user_id = get_current_user_id(); + } + return '1' === get_user_meta( $user_id, 'paying_customer', true ); + } + + /** + * Legacy get address. + */ + function get_address() { + wc_deprecated_function( 'WC_Customer::get_address', '3.0', 'WC_Customer::get_billing_address_1' ); + return $this->get_billing_address_1(); + } + + /** + * Legacy get address 2. + */ + function get_address_2() { + wc_deprecated_function( 'WC_Customer::get_address_2', '3.0', 'WC_Customer::get_billing_address_2' ); + return $this->get_billing_address_2(); + } + + /** + * Legacy get country. + */ + function get_country() { + wc_deprecated_function( 'WC_Customer::get_country', '3.0', 'WC_Customer::get_billing_country' ); + return $this->get_billing_country(); + } + + /** + * Legacy get state. + */ + function get_state() { + wc_deprecated_function( 'WC_Customer::get_state', '3.0', 'WC_Customer::get_billing_state' ); + return $this->get_billing_state(); + } + + /** + * Legacy get postcode. + */ + function get_postcode() { + wc_deprecated_function( 'WC_Customer::get_postcode', '3.0', 'WC_Customer::get_billing_postcode' ); + return $this->get_billing_postcode(); + } + + /** + * Legacy get city. + */ + function get_city() { + wc_deprecated_function( 'WC_Customer::get_city', '3.0', 'WC_Customer::get_billing_city' ); + return $this->get_billing_city(); + } + + /** + * Legacy set country. + * + * @param string $country + */ + function set_country( $country ) { + wc_deprecated_function( 'WC_Customer::set_country', '3.0', 'WC_Customer::set_billing_country' ); + $this->set_billing_country( $country ); + } + + /** + * Legacy set state. + * + * @param string $state + */ + function set_state( $state ) { + wc_deprecated_function( 'WC_Customer::set_state', '3.0', 'WC_Customer::set_billing_state' ); + $this->set_billing_state( $state ); + } + + /** + * Legacy set postcode. + * + * @param string $postcode + */ + function set_postcode( $postcode ) { + wc_deprecated_function( 'WC_Customer::set_postcode', '3.0', 'WC_Customer::set_billing_postcode' ); + $this->set_billing_postcode( $postcode ); + } + + /** + * Legacy set city. + * + * @param string $city + */ + function set_city( $city ) { + wc_deprecated_function( 'WC_Customer::set_city', '3.0', 'WC_Customer::set_billing_city' ); + $this->set_billing_city( $city ); + } + + /** + * Legacy set address. + * + * @param string $address + */ + function set_address( $address ) { + wc_deprecated_function( 'WC_Customer::set_address', '3.0', 'WC_Customer::set_billing_address' ); + $this->set_billing_address( $address ); + } + + /** + * Legacy set address. + * + * @param string $address + */ + function set_address_2( $address ) { + wc_deprecated_function( 'WC_Customer::set_address_2', '3.0', 'WC_Customer::set_billing_address_2' ); + $this->set_billing_address_2( $address ); + } +} diff --git a/includes/legacy/class-wc-legacy-shipping-zone.php b/includes/legacy/class-wc-legacy-shipping-zone.php new file mode 100644 index 00000000000..aa06337a36c --- /dev/null +++ b/includes/legacy/class-wc-legacy-shipping-zone.php @@ -0,0 +1,68 @@ +get_id(); + } + + /** + * Read a shipping zone by ID. + * @deprecated 3.0.0 - Init a shipping zone with an ID. + * + * @param int $zone_id + */ + public function read( $zone_id ) { + wc_deprecated_function( 'WC_Shipping_Zone::read', '3.0', 'a shipping zone initialized with an ID.' ); + $this->set_id( $zone_id ); + $data_store = WC_Data_Store::load( 'shipping-zone' ); + $data_store->read( $this ); + } + + /** + * Update a zone. + * @deprecated 3.0.0 - Use ::save instead. + */ + public function update() { + wc_deprecated_function( 'WC_Shipping_Zone::update', '3.0', 'WC_Shipping_Zone::save instead.' ); + $data_store = WC_Data_Store::load( 'shipping-zone' ); + try { + $data_store->update( $this ); + } catch ( Exception $e ) { + return false; + } + } + + /** + * Create a zone. + * @deprecated 3.0.0 - Use ::save instead. + */ + public function create() { + wc_deprecated_function( 'WC_Shipping_Zone::create', '3.0', 'WC_Shipping_Zone::save instead.' ); + $data_store = WC_Data_Store::load( 'shipping-zone' ); + try { + $data_store->create( $this ); + } catch ( Exception $e ) { + return false; + } + } + + +} diff --git a/includes/libraries/class-cssmin.php b/includes/libraries/class-cssmin.php deleted file mode 100644 index 198e9db4feb..00000000000 --- a/includes/libraries/class-cssmin.php +++ /dev/null @@ -1,5085 +0,0 @@ - - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * -- - * - * @package CssMin - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ - -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly - -/** - * Abstract definition of a CSS token class. - * - * Every token has to extend this class. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssToken - { - /** - * Returns the token as string. - * - * @return string - */ - abstract public function __toString(); - } - -/** - * Abstract definition of a for a ruleset start token. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssRulesetStartToken extends aCssToken - { - - } - -/** - * Abstract definition of a for ruleset end token. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssRulesetEndToken extends aCssToken - { - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return "}"; - } - } - -/** - * Abstract definition of a parser plugin. - * - * Every parser plugin have to extend this class. A parser plugin contains the logic to parse one or aspects of a - * stylesheet. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssParserPlugin - { - /** - * Plugin configuration. - * - * @var array - */ - protected $configuration = array(); - /** - * The CssParser of the plugin. - * - * @var CssParser - */ - protected $parser = null; - /** - * Plugin buffer. - * - * @var string - */ - protected $buffer = ""; - /** - * Constructor. - * - * @param CssParser $parser The CssParser object of this plugin. - * @param array $configuration Plugin configuration [optional] - * @return void - */ - public function __construct(CssParser $parser, array $configuration = null) - { - $this->configuration = $configuration; - $this->parser = $parser; - } - /** - * Returns the array of chars triggering the parser plugin. - * - * @return array - */ - abstract public function getTriggerChars(); - /** - * Returns the array of states triggering the parser plugin or FALSE if every state will trigger the parser plugin. - * - * @return array - */ - abstract public function getTriggerStates(); - /** - * Parser routine of the plugin. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - abstract public function parse($index, $char, $previousChar, $state); - } - -/** - * Abstract definition of a minifier plugin class. - * - * Minifier plugin process the parsed tokens one by one to apply changes to the token. Every minifier plugin has to - * extend this class. - * - * @package CssMin/Minifier/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssMinifierPlugin - { - /** - * Plugin configuration. - * - * @var array - */ - protected $configuration = array(); - /** - * The CssMinifier of the plugin. - * - * @var CssMinifier - */ - protected $minifier = null; - /** - * Constructor. - * - * @param CssMinifier $minifier The CssMinifier object of this plugin. - * @param array $configuration Plugin configuration [optional] - * @return void - */ - public function __construct(CssMinifier $minifier, array $configuration = array()) - { - $this->configuration = $configuration; - $this->minifier = $minifier; - } - /** - * Apply the plugin to the token. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - abstract public function apply(aCssToken &$token); - /** - * -- - * - * @return array - */ - abstract public function getTriggerTokens(); - } - -/** - * Abstract definition of a minifier filter class. - * - * Minifier filters allows a pre-processing of the parsed token to add, edit or delete tokens. Every minifier filter - * has to extend this class. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssMinifierFilter - { - /** - * Filter configuration. - * - * @var array - */ - protected $configuration = array(); - /** - * The CssMinifier of the filter. - * - * @var CssMinifier - */ - protected $minifier = null; - /** - * Constructor. - * - * @param CssMinifier $minifier The CssMinifier object of this plugin. - * @param array $configuration Filter configuration [optional] - * @return void - */ - public function __construct(CssMinifier $minifier, array $configuration = array()) - { - $this->configuration = $configuration; - $this->minifier = $minifier; - } - /** - * Filter the tokens. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value large than 0 will rebuild the array - */ - abstract public function apply(array &$tokens); - } - -/** - * Abstract formatter definition. - * - * Every formatter have to extend this class. - * - * @package CssMin/Formatter - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssFormatter - { - /** - * Indent string. - * - * @var string - */ - protected $indent = " "; - /** - * Declaration padding. - * - * @var integer - */ - protected $padding = 0; - /** - * Tokens. - * - * @var array - */ - protected $tokens = array(); - /** - * Constructor. - * - * @param array $tokens Array of CssToken - * @param string $indent Indent string [optional] - * @param integer $padding Declaration value padding [optional] - */ - public function __construct(array $tokens, $indent = null, $padding = null) - { - $this->tokens = $tokens; - $this->indent = !is_null($indent) ? $indent : $this->indent; - $this->padding = !is_null($padding) ? $padding : $this->padding; - } - /** - * Returns the array of aCssToken as formatted string. - * - * @return string - */ - abstract public function __toString(); - } - -/** - * Abstract definition of a ruleset declaration token. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssDeclarationToken extends aCssToken - { - /** - * Is the declaration flagged as important? - * - * @var boolean - */ - public $IsImportant = false; - /** - * Is the declaration flagged as last one of the ruleset? - * - * @var boolean - */ - public $IsLast = false; - /** - * Property name of the declaration. - * - * @var string - */ - public $Property = ""; - /** - * Value of the declaration. - * - * @var string - */ - public $Value = ""; - /** - * Set the properties of the @font-face declaration. - * - * @param string $property Property of the declaration - * @param string $value Value of the declaration - * @param boolean $isImportant Is the !important flag is set? - * @param boolean $IsLast Is the declaration the last one of the block? - * @return void - */ - public function __construct($property, $value, $isImportant = false, $isLast = false) - { - $this->Property = $property; - $this->Value = $value; - $this->IsImportant = $isImportant; - $this->IsLast = $isLast; - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return $this->Property . ":" . $this->Value . ($this->IsImportant ? " !important" : "") . ($this->IsLast ? "" : ";"); - } - } - -/** - * Abstract definition of a for at-rule block start token. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssAtBlockStartToken extends aCssToken - { - - } - -/** - * Abstract definition of a for at-rule block end token. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -abstract class aCssAtBlockEndToken extends aCssToken - { - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return "}"; - } - } - -/** - * {@link aCssFromatter Formatter} returning the CSS source in {@link http://goo.gl/etzLs Whitesmiths indent style}. - * - * @package CssMin/Formatter - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssWhitesmithsFormatter extends aCssFormatter - { - /** - * Implements {@link aCssFormatter::__toString()}. - * - * @return string - */ - public function __toString() - { - $r = array(); - $level = 0; - for ($i = 0, $l = count($this->tokens); $i < $l; $i++) - { - $token = $this->tokens[$i]; - $class = get_class($token); - $indent = str_repeat($this->indent, $level); - if ($class === "CssCommentToken") - { - $lines = array_map("trim", explode("\n", $token->Comment)); - for ($ii = 0, $ll = count($lines); $ii < $ll; $ii++) - { - $r[] = $indent . (substr($lines[$ii], 0, 1) == "*" ? " " : "") . $lines[$ii]; - } - } - elseif ($class === "CssAtCharsetToken") - { - $r[] = $indent . "@charset " . $token->Charset . ";"; - } - elseif ($class === "CssAtFontFaceStartToken") - { - $r[] = $indent . "@font-face"; - $r[] = $this->indent . $indent . "{"; - $level++; - } - elseif ($class === "CssAtImportToken") - { - $r[] = $indent . "@import " . $token->Import . " " . implode(", ", $token->MediaTypes) . ";"; - } - elseif ($class === "CssAtKeyframesStartToken") - { - $r[] = $indent . "@keyframes \"" . $token->Name . "\""; - $r[] = $this->indent . $indent . "{"; - $level++; - } - elseif ($class === "CssAtMediaStartToken") - { - $r[] = $indent . "@media " . implode(", ", $token->MediaTypes); - $r[] = $this->indent . $indent . "{"; - $level++; - } - elseif ($class === "CssAtPageStartToken") - { - $r[] = $indent . "@page"; - $r[] = $this->indent . $indent . "{"; - $level++; - } - elseif ($class === "CssAtVariablesStartToken") - { - $r[] = $indent . "@variables " . implode(", ", $token->MediaTypes); - $r[] = $this->indent . $indent . "{"; - $level++; - } - elseif ($class === "CssRulesetStartToken" || $class === "CssAtKeyframesRulesetStartToken") - { - $r[] = $indent . implode(", ", $token->Selectors); - $r[] = $this->indent . $indent . "{"; - $level++; - } - elseif ($class == "CssAtFontFaceDeclarationToken" - || $class === "CssAtKeyframesRulesetDeclarationToken" - || $class === "CssAtPageDeclarationToken" - || $class == "CssAtVariablesDeclarationToken" - || $class === "CssRulesetDeclarationToken" - ) - { - $declaration = $indent . $token->Property . ": "; - if ($this->padding) - { - $declaration = str_pad($declaration, $this->padding, " ", STR_PAD_RIGHT); - } - $r[] = $declaration . $token->Value . ($token->IsImportant ? " !important" : "") . ";"; - } - elseif ($class === "CssAtFontFaceEndToken" - || $class === "CssAtMediaEndToken" - || $class === "CssAtKeyframesEndToken" - || $class === "CssAtKeyframesRulesetEndToken" - || $class === "CssAtPageEndToken" - || $class === "CssAtVariablesEndToken" - || $class === "CssRulesetEndToken" - ) - { - $r[] = $indent . "}"; - $level--; - } - } - return implode("\n", $r); - } - } - -/** - * This {@link aCssMinifierPlugin} will process var-statement and sets the declaration value to the variable value. - * - * This plugin only apply the variable values. The variable values itself will get parsed by the - * {@link CssVariablesMinifierFilter}. - * - * Example: - * - * @variables - * { - * defaultColor: black; - * } - * color: var(defaultColor); - * - * - * Will get converted to: - * - * color:black; - * - * - * @package CssMin/Minifier/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssVariablesMinifierPlugin extends aCssMinifierPlugin - { - /** - * Regular expression matching a value. - * - * @var string - */ - private $reMatch = "/var\((.+)\)/iSU"; - /** - * Parsed variables. - * - * @var array - */ - private $variables = null; - /** - * Returns the variables. - * - * @return array - */ - public function getVariables() - { - return $this->variables; - } - /** - * Implements {@link aCssMinifierPlugin::minify()}. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - public function apply(aCssToken &$token) - { - if (stripos($token->Value, "var") !== false && preg_match_all($this->reMatch, $token->Value, $m)) - { - $mediaTypes = $token->MediaTypes; - if (!in_array("all", $mediaTypes)) - { - $mediaTypes[] = "all"; - } - for ($i = 0, $l = count($m[0]); $i < $l; $i++) - { - $variable = trim($m[1][$i]); - foreach ($mediaTypes as $mediaType) - { - if (isset($this->variables[$mediaType], $this->variables[$mediaType][$variable])) - { - // Variable value found => set the declaration value to the variable value and return - $token->Value = str_replace($m[0][$i], $this->variables[$mediaType][$variable], $token->Value); - continue 2; - } - } - // If no value was found trigger an error and replace the token with a CssNullToken - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": No value found for variable " . $variable . " in media types " . implode(", ", $mediaTypes) . "", (string) $token)); - $token = new CssNullToken(); - return true; - } - } - return false; - } - /** - * Implements {@link aMinifierPlugin::getTriggerTokens()} - * - * @return array - */ - public function getTriggerTokens() - { - return array - ( - "CssAtFontFaceDeclarationToken", - "CssAtPageDeclarationToken", - "CssRulesetDeclarationToken" - ); - } - /** - * Sets the variables. - * - * @param array $variables Variables to set - * @return void - */ - public function setVariables(array $variables) - { - $this->variables = $variables; - } - } - -/** - * This {@link aCssMinifierFilter minifier filter} will parse the variable declarations out of @variables at-rule - * blocks. The variables will get store in the {@link CssVariablesMinifierPlugin} that will apply the variables to - * declaration. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssVariablesMinifierFilter extends aCssMinifierFilter - { - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value large than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - $variables = array(); - $defaultMediaTypes = array("all"); - $mediaTypes = array(); - $remove = array(); - for($i = 0, $l = count($tokens); $i < $l; $i++) - { - // @variables at-rule block found - if (get_class($tokens[$i]) === "CssAtVariablesStartToken") - { - $remove[] = $i; - $mediaTypes = (count($tokens[$i]->MediaTypes) == 0 ? $defaultMediaTypes : $tokens[$i]->MediaTypes); - foreach ($mediaTypes as $mediaType) - { - if (!isset($variables[$mediaType])) - { - $variables[$mediaType] = array(); - } - } - // Read the variable declaration tokens - for($i = $i; $i < $l; $i++) - { - // Found a variable declaration => read the variable values - if (get_class($tokens[$i]) === "CssAtVariablesDeclarationToken") - { - foreach ($mediaTypes as $mediaType) - { - $variables[$mediaType][$tokens[$i]->Property] = $tokens[$i]->Value; - } - $remove[] = $i; - } - // Found the variables end token => break; - elseif (get_class($tokens[$i]) === "CssAtVariablesEndToken") - { - $remove[] = $i; - break; - } - } - } - } - // Variables in @variables at-rule blocks - foreach($variables as $mediaType => $null) - { - foreach($variables[$mediaType] as $variable => $value) - { - // If a var() statement in a variable value found... - if (stripos($value, "var") !== false && preg_match_all("/var\((.+)\)/iSU", $value, $m)) - { - // ... then replace the var() statement with the variable values. - for ($i = 0, $l = count($m[0]); $i < $l; $i++) - { - $variables[$mediaType][$variable] = str_replace($m[0][$i], (isset($variables[$mediaType][$m[1][$i]]) ? $variables[$mediaType][$m[1][$i]] : ""), $variables[$mediaType][$variable]); - } - } - } - } - // Remove the complete @variables at-rule block - foreach ($remove as $i) - { - $tokens[$i] = null; - } - if (!($plugin = $this->minifier->getPlugin("CssVariablesMinifierPlugin"))) - { - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": The plugin CssVariablesMinifierPlugin was not found but is required for " . __CLASS__ . "")); - } - else - { - $plugin->setVariables($variables); - } - return count($remove); - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for preserve parsing url() values. - * - * This plugin return no {@link aCssToken CssToken} but ensures that url() values will get parsed properly. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssUrlParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("(", ")"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return false; - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - // Start of string - if ($char === "(" && strtolower(substr($this->parser->getSource(), $index - 3, 4)) === "url(" && $state !== "T_URL") - { - $this->parser->pushState("T_URL"); - $this->parser->setExclusive(__CLASS__); - } - // Escaped LF in url => remove escape backslash and LF - elseif ($char === "\n" && $previousChar === "\\" && $state === "T_URL") - { - $this->parser->setBuffer(substr($this->parser->getBuffer(), 0, -2)); - } - // Parse error: Unescaped LF in string literal - elseif ($char === "\n" && $previousChar !== "\\" && $state === "T_URL") - { - $line = $this->parser->getBuffer(); - $this->parser->setBuffer(substr($this->parser->getBuffer(), 0, -1) . ")"); // Replace the LF with the url string delimiter - $this->parser->popState(); - $this->parser->unsetExclusive(); - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Unterminated string literal", $line . "_")); - } - // End of string - elseif ($char === ")" && $state === "T_URL") - { - $this->parser->popState(); - $this->parser->unsetExclusive(); - } - else - { - return false; - } - return true; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for preserve parsing string values. - * - * This plugin return no {@link aCssToken CssToken} but ensures that string values will get parsed properly. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssStringParserPlugin extends aCssParserPlugin - { - /** - * Current string delimiter char. - * - * @var string - */ - private $delimiterChar = null; - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("\"", "'", "\n"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return false; - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - // Start of string - if (($char === "\"" || $char === "'") && $state !== "T_STRING") - { - $this->delimiterChar = $char; - $this->parser->pushState("T_STRING"); - $this->parser->setExclusive(__CLASS__); - } - // Escaped LF in string => remove escape backslash and LF - elseif ($char === "\n" && $previousChar === "\\" && $state === "T_STRING") - { - $this->parser->setBuffer(substr($this->parser->getBuffer(), 0, -2)); - } - // Parse error: Unescaped LF in string literal - elseif ($char === "\n" && $previousChar !== "\\" && $state === "T_STRING") - { - $line = $this->parser->getBuffer(); - $this->parser->popState(); - $this->parser->unsetExclusive(); - $this->parser->setBuffer(substr($this->parser->getBuffer(), 0, -1) . $this->delimiterChar); // Replace the LF with the current string char - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Unterminated string literal", $line . "_")); - $this->delimiterChar = null; - } - // End of string - elseif ($char === $this->delimiterChar && $state === "T_STRING") - { - // If the Previous char is a escape char count the amount of the previous escape chars. If the amount of - // escape chars is uneven do not end the string - if ($previousChar == "\\") - { - $source = $this->parser->getSource(); - $c = 1; - $i = $index - 2; - while (substr($source, $i, 1) === "\\") - { - $c++; $i--; - } - if ($c % 2) - { - return false; - } - } - $this->parser->popState(); - $this->parser->unsetExclusive(); - $this->delimiterChar = null; - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssMinifierFilter minifier filter} sorts the ruleset declarations of a ruleset by name. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Rowan Beentje - * @copyright Rowan Beentje - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssSortRulesetPropertiesMinifierFilter extends aCssMinifierFilter - { - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value larger than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - $r = 0; - for ($i = 0, $l = count($tokens); $i < $l; $i++) - { - // Only look for ruleset start rules - if (get_class($tokens[$i]) !== "CssRulesetStartToken") { continue; } - // Look for the corresponding ruleset end - $endIndex = false; - for ($ii = $i + 1; $ii < $l; $ii++) - { - if (get_class($tokens[$ii]) !== "CssRulesetEndToken") { continue; } - $endIndex = $ii; - break; - } - if (!$endIndex) { break; } - $startIndex = $i; - $i = $endIndex; - // Skip if there's only one token in this ruleset - if ($endIndex - $startIndex <= 2) { continue; } - // Ensure that everything between the start and end is a declaration token, for safety - for ($ii = $startIndex + 1; $ii < $endIndex; $ii++) - { - if (get_class($tokens[$ii]) !== "CssRulesetDeclarationToken") { continue(2); } - } - $declarations = array_slice($tokens, $startIndex + 1, $endIndex - $startIndex - 1); - // Check whether a sort is required - $sortRequired = $lastPropertyName = false; - foreach ($declarations as $declaration) - { - if ($lastPropertyName) - { - if (strcmp($lastPropertyName, $declaration->Property) > 0) - { - $sortRequired = true; - break; - } - } - $lastPropertyName = $declaration->Property; - } - if (!$sortRequired) { continue; } - // Arrange the declarations alphabetically by name - usort($declarations, array(__CLASS__, "userDefinedSort1")); - // Update "IsLast" property - for ($ii = 0, $ll = count($declarations) - 1; $ii <= $ll; $ii++) - { - if ($ii == $ll) - { - $declarations[$ii]->IsLast = true; - } - else - { - $declarations[$ii]->IsLast = false; - } - } - // Splice back into the array. - array_splice($tokens, $startIndex + 1, $endIndex - $startIndex - 1, $declarations); - $r += $endIndex - $startIndex - 1; - } - return $r; - } - /** - * User defined sort function. - * - * @return integer - */ - public static function userDefinedSort1($a, $b) - { - return strcmp($a->Property, $b->Property); - } - } - -/** - * This {@link aCssToken CSS token} represents the start of a ruleset. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssRulesetStartToken extends aCssRulesetStartToken - { - /** - * Array of selectors. - * - * @var array - */ - public $Selectors = array(); - /** - * Set the properties of a ruleset token. - * - * @param array $selectors Selectors of the ruleset - * @return void - */ - public function __construct(array $selectors = array()) - { - $this->Selectors = $selectors; - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return implode(",", $this->Selectors) . "{"; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing ruleset block with including declarations. - * - * Found rulesets will add a {@link CssRulesetStartToken} and {@link CssRulesetEndToken} to the - * parser; including declarations as {@link CssRulesetDeclarationToken}. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssRulesetParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array(",", "{", "}", ":", ";"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return array("T_DOCUMENT", "T_AT_MEDIA", "T_RULESET::SELECTORS", "T_RULESET", "T_RULESET_DECLARATION"); - } - /** - * Selectors. - * - * @var array - */ - private $selectors = array(); - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - // Start of Ruleset and selectors - if ($char === "," && ($state === "T_DOCUMENT" || $state === "T_AT_MEDIA" || $state === "T_RULESET::SELECTORS")) - { - if ($state !== "T_RULESET::SELECTORS") - { - $this->parser->pushState("T_RULESET::SELECTORS"); - } - $this->selectors[] = $this->parser->getAndClearBuffer(",{"); - } - // End of selectors and start of declarations - elseif ($char === "{" && ($state === "T_DOCUMENT" || $state === "T_AT_MEDIA" || $state === "T_RULESET::SELECTORS")) - { - if ($this->parser->getBuffer() !== "") - { - $this->selectors[] = $this->parser->getAndClearBuffer(",{"); - if ($state == "T_RULESET::SELECTORS") - { - $this->parser->popState(); - } - $this->parser->pushState("T_RULESET"); - $this->parser->appendToken(new CssRulesetStartToken($this->selectors)); - $this->selectors = array(); - } - } - // Start of declaration - elseif ($char === ":" && $state === "T_RULESET") - { - $this->parser->pushState("T_RULESET_DECLARATION"); - $this->buffer = $this->parser->getAndClearBuffer(":;", true); - } - // Unterminated ruleset declaration - elseif ($char === ":" && $state === "T_RULESET_DECLARATION") - { - // Ignore Internet Explorer filter declarations - if ($this->buffer === "filter") - { - return false; - } - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Unterminated declaration", $this->buffer . ":" . $this->parser->getBuffer() . "_")); - } - // End of declaration - elseif (($char === ";" || $char === "}") && $state === "T_RULESET_DECLARATION") - { - $value = $this->parser->getAndClearBuffer(";}"); - if (strtolower(substr($value, -10, 10)) === "!important") - { - $value = trim(substr($value, 0, -10)); - $isImportant = true; - } - else - { - $isImportant = false; - } - $this->parser->popState(); - $this->parser->appendToken(new CssRulesetDeclarationToken($this->buffer, $value, $this->parser->getMediaTypes(), $isImportant)); - // Declaration ends with a right curly brace; so we have to end the ruleset - if ($char === "}") - { - $this->parser->appendToken(new CssRulesetEndToken()); - $this->parser->popState(); - } - $this->buffer = ""; - } - // End of ruleset - elseif ($char === "}" && $state === "T_RULESET") - { - $this->parser->popState(); - $this->parser->clearBuffer(); - $this->parser->appendToken(new CssRulesetEndToken()); - $this->buffer = ""; - $this->selectors = array(); - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssToken CSS token} represents the end of a ruleset. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssRulesetEndToken extends aCssRulesetEndToken - { - - } - -/** - * This {@link aCssToken CSS token} represents a ruleset declaration. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssRulesetDeclarationToken extends aCssDeclarationToken - { - /** - * Media types of the declaration. - * - * @var array - */ - public $MediaTypes = array("all"); - /** - * Set the properties of a ddocument- or at-rule @media level declaration. - * - * @param string $property Property of the declaration - * @param string $value Value of the declaration - * @param mixed $mediaTypes Media types of the declaration - * @param boolean $isImportant Is the !important flag is set - * @param boolean $isLast Is the declaration the last one of the ruleset - * @return void - */ - public function __construct($property, $value, $mediaTypes = null, $isImportant = false, $isLast = false) - { - parent::__construct($property, $value, $isImportant, $isLast); - $this->MediaTypes = $mediaTypes ? $mediaTypes : array("all"); - } - } - -/** - * This {@link aCssMinifierFilter minifier filter} sets the IsLast property of any last declaration in a ruleset, - * @font-face at-rule or @page at-rule block. If the property IsLast is TRUE the decrations will get stringified - * without tailing semicolon. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssRemoveLastDelarationSemiColonMinifierFilter extends aCssMinifierFilter - { - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value large than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - for ($i = 0, $l = count($tokens); $i < $l; $i++) - { - $current = get_class($tokens[$i]); - $next = isset($tokens[$i+1]) ? get_class($tokens[$i+1]) : false; - if (($current === "CssRulesetDeclarationToken" && $next === "CssRulesetEndToken") || - ($current === "CssAtFontFaceDeclarationToken" && $next === "CssAtFontFaceEndToken") || - ($current === "CssAtPageDeclarationToken" && $next === "CssAtPageEndToken")) - { - $tokens[$i]->IsLast = true; - } - } - return 0; - } - } - -/** - * This {@link aCssMinifierFilter minifier filter} will remove any empty rulesets (including @keyframes at-rule block - * rulesets). - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssRemoveEmptyRulesetsMinifierFilter extends aCssMinifierFilter - { - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value large than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - $r = 0; - for ($i = 0, $l = count($tokens); $i < $l; $i++) - { - $current = get_class($tokens[$i]); - $next = isset($tokens[$i + 1]) ? get_class($tokens[$i + 1]) : false; - if (($current === "CssRulesetStartToken" && $next === "CssRulesetEndToken") || - ($current === "CssAtKeyframesRulesetStartToken" && $next === "CssAtKeyframesRulesetEndToken" && !array_intersect(array("from", "0%", "to", "100%"), array_map("strtolower", $tokens[$i]->Selectors))) - ) - { - $tokens[$i] = null; - $tokens[$i + 1] = null; - $i++; - $r = $r + 2; - } - } - return $r; - } - } - -/** - * This {@link aCssMinifierFilter minifier filter} will remove any empty @font-face, @keyframes, @media and @page - * at-rule blocks. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssRemoveEmptyAtBlocksMinifierFilter extends aCssMinifierFilter - { - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value large than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - $r = 0; - for ($i = 0, $l = count($tokens); $i < $l; $i++) - { - $current = get_class($tokens[$i]); - $next = isset($tokens[$i + 1]) ? get_class($tokens[$i + 1]) : false; - if (($current === "CssAtFontFaceStartToken" && $next === "CssAtFontFaceEndToken") || - ($current === "CssAtKeyframesStartToken" && $next === "CssAtKeyframesEndToken") || - ($current === "CssAtPageStartToken" && $next === "CssAtPageEndToken") || - ($current === "CssAtMediaStartToken" && $next === "CssAtMediaEndToken")) - { - $tokens[$i] = null; - $tokens[$i + 1] = null; - $i++; - $r = $r + 2; - } - } - return $r; - } - } - -/** - * This {@link aCssMinifierFilter minifier filter} will remove any comments from the array of parsed tokens. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssRemoveCommentsMinifierFilter extends aCssMinifierFilter - { - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value large than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - $r = 0; - for ($i = 0, $l = count($tokens); $i < $l; $i++) - { - if (get_class($tokens[$i]) === "CssCommentToken") - { - $tokens[$i] = null; - $r++; - } - } - return $r; - } - } - -/** - * CSS Parser. - * - * @package CssMin/Parser - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssParser - { - /** - * Parse buffer. - * - * @var string - */ - private $buffer = ""; - /** - * {@link aCssParserPlugin Plugins}. - * - * @var array - */ - private $plugins = array(); - /** - * Source to parse. - * - * @var string - */ - private $source = ""; - /** - * Current state. - * - * @var integer - */ - private $state = "T_DOCUMENT"; - /** - * Exclusive state. - * - * @var string - */ - private $stateExclusive = false; - /** - * Media types state. - * - * @var mixed - */ - private $stateMediaTypes = false; - /** - * State stack. - * - * @var array - */ - private $states = array("T_DOCUMENT"); - /** - * Parsed tokens. - * - * @var array - */ - private $tokens = array(); - /** - * Constructer. - * - * Create instances of the used {@link aCssParserPlugin plugins}. - * - * @param string $source CSS source [optional] - * @param array $plugins Plugin configuration [optional] - * @return void - */ - public function __construct($source = null, array $plugins = null) - { - $plugins = array_merge(array - ( - "Comment" => true, - "String" => true, - "Url" => true, - "Expression" => true, - "Ruleset" => true, - "AtCharset" => true, - "AtFontFace" => true, - "AtImport" => true, - "AtKeyframes" => true, - "AtMedia" => true, - "AtPage" => true, - "AtVariables" => true - ), is_array($plugins) ? $plugins : array()); - // Create plugin instances - foreach ($plugins as $name => $config) - { - if ($config !== false) - { - $class = "Css" . $name . "ParserPlugin"; - $config = is_array($config) ? $config : array(); - if (class_exists($class)) - { - $this->plugins[] = new $class($this, $config); - } - else - { - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": The plugin " . $name . " with the class name " . $class . " was not found")); - } - } - } - if (!is_null($source)) - { - $this->parse($source); - } - } - /** - * Append a token to the array of tokens. - * - * @param aCssToken $token Token to append - * @return void - */ - public function appendToken(aCssToken $token) - { - $this->tokens[] = $token; - } - /** - * Clears the current buffer. - * - * @return void - */ - public function clearBuffer() - { - $this->buffer = ""; - } - /** - * Returns and clear the current buffer. - * - * @param string $trim Chars to use to trim the returned buffer - * @param boolean $tolower if TRUE the returned buffer will get converted to lower case - * @return string - */ - public function getAndClearBuffer($trim = "", $tolower = false) - { - $r = $this->getBuffer($trim, $tolower); - $this->buffer = ""; - return $r; - } - /** - * Returns the current buffer. - * - * @param string $trim Chars to use to trim the returned buffer - * @param boolean $tolower if TRUE the returned buffer will get converted to lower case - * @return string - */ - public function getBuffer($trim = "", $tolower = false) - { - $r = $this->buffer; - if ($trim) - { - $r = trim($r, " \t\n\r\0\x0B" . $trim); - } - if ($tolower) - { - $r = strtolower($r); - } - return $r; - } - /** - * Returns the current media types state. - * - * @return array - */ - public function getMediaTypes() - { - return $this->stateMediaTypes; - } - /** - * Returns the CSS source. - * - * @return string - */ - public function getSource() - { - return $this->source; - } - /** - * Returns the current state. - * - * @return integer The current state - */ - public function getState() - { - return $this->state; - } - /** - * Returns a plugin by class name. - * - * @param string $name Class name of the plugin - * @return aCssParserPlugin - */ - public function getPlugin($class) - { - static $index = null; - if (is_null($index)) - { - $index = array(); - for ($i = 0, $l = count($this->plugins); $i < $l; $i++) - { - $index[get_class($this->plugins[$i])] = $i; - } - } - return isset($index[$class]) ? $this->plugins[$index[$class]] : false; - } - /** - * Returns the parsed tokens. - * - * @return array - */ - public function getTokens() - { - return $this->tokens; - } - /** - * Returns if the current state equals the passed state. - * - * @param integer $state State to compare with the current state - * @return boolean TRUE is the state equals to the passed state; FALSE if not - */ - public function isState($state) - { - return ($this->state == $state); - } - /** - * Parse the CSS source and return a array with parsed tokens. - * - * @param string $source CSS source - * @return array Array with tokens - */ - public function parse($source) - { - // Reset - $this->source = ""; - $this->tokens = array(); - // Create a global and plugin lookup table for trigger chars; set array of plugins as local variable and create - // several helper variables for plugin handling - $globalTriggerChars = ""; - $plugins = $this->plugins; - $pluginCount = count($plugins); - $pluginIndex = array(); - $pluginTriggerStates = array(); - $pluginTriggerChars = array(); - for ($i = 0, $l = count($plugins); $i < $l; $i++) - { - $tPluginClassName = get_class($plugins[$i]); - $pluginTriggerChars[$i] = implode("", $plugins[$i]->getTriggerChars()); - $tPluginTriggerStates = $plugins[$i]->getTriggerStates(); - $pluginTriggerStates[$i] = $tPluginTriggerStates === false ? false : "|" . implode("|", $tPluginTriggerStates) . "|"; - $pluginIndex[$tPluginClassName] = $i; - for ($ii = 0, $ll = strlen($pluginTriggerChars[$i]); $ii < $ll; $ii++) - { - $c = substr($pluginTriggerChars[$i], $ii, 1); - if (strpos($globalTriggerChars, $c) === false) - { - $globalTriggerChars .= $c; - } - } - } - // Normalise line endings - $source = str_replace("\r\n", "\n", $source); // Windows to Unix line endings - $source = str_replace("\r", "\n", $source); // Mac to Unix line endings - $this->source = $source; - // Variables - $buffer = &$this->buffer; - $exclusive = &$this->stateExclusive; - $state = &$this->state; - $c = $p = null; - // -- - for ($i = 0, $l = strlen($source); $i < $l; $i++) - { - // Set the current Char - $c = $source[$i]; // Is faster than: $c = substr($source, $i, 1); - // Normalize and filter double whitespace characters - if ($exclusive === false) - { - if ($c === "\n" || $c === "\t") - { - $c = " "; - } - if ($c === " " && $p === " ") - { - continue; - } - } - $buffer .= $c; - // Extended processing only if the current char is a global trigger char - if (strpos($globalTriggerChars, $c) !== false) - { - // Exclusive state is set; process with the exclusive plugin - if ($exclusive) - { - $tPluginIndex = $pluginIndex[$exclusive]; - if (strpos($pluginTriggerChars[$tPluginIndex], $c) !== false && ($pluginTriggerStates[$tPluginIndex] === false || strpos($pluginTriggerStates[$tPluginIndex], $state) !== false)) - { - $r = $plugins[$tPluginIndex]->parse($i, $c, $p, $state); - // Return value is TRUE => continue with next char - if ($r === true) - { - continue; - } - // Return value is numeric => set new index and continue with next char - elseif ($r !== false && $r != $i) - { - $i = $r; - continue; - } - } - } - // Else iterate through the plugins - else - { - $triggerState = "|" . $state . "|"; - for ($ii = 0, $ll = $pluginCount; $ii < $ll; $ii++) - { - // Only process if the current char is one of the plugin trigger chars - if (strpos($pluginTriggerChars[$ii], $c) !== false && ($pluginTriggerStates[$ii] === false || strpos($pluginTriggerStates[$ii], $triggerState) !== false)) - { - // Process with the plugin - $r = $plugins[$ii]->parse($i, $c, $p, $state); - // Return value is TRUE => break the plugin loop and and continue with next char - if ($r === true) - { - break; - } - // Return value is numeric => set new index, break the plugin loop and and continue with next char - elseif ($r !== false && $r != $i) - { - $i = $r; - break; - } - } - } - } - } - $p = $c; // Set the parent char - } - return $this->tokens; - } - /** - * Remove the last state of the state stack and return the removed stack value. - * - * @return integer Removed state value - */ - public function popState() - { - $r = array_pop($this->states); - $this->state = $this->states[count($this->states) - 1]; - return $r; - } - /** - * Adds a new state onto the state stack. - * - * @param integer $state State to add onto the state stack. - * @return integer The index of the added state in the state stacks - */ - public function pushState($state) - { - $r = array_push($this->states, $state); - $this->state = $this->states[count($this->states) - 1]; - return $r; - } - /** - * Sets/restores the buffer. - * - * @param string $buffer Buffer to set - * @return void - */ - public function setBuffer($buffer) - { - $this->buffer = $buffer; - } - /** - * Set the exclusive state. - * - * @param string $exclusive Exclusive state - * @return void - */ - public function setExclusive($exclusive) - { - $this->stateExclusive = $exclusive; - } - /** - * Set the media types state. - * - * @param array $mediaTypes Media types state - * @return void - */ - public function setMediaTypes(array $mediaTypes) - { - $this->stateMediaTypes = $mediaTypes; - } - /** - * Sets the current state in the state stack; equals to {@link CssParser::popState()} + {@link CssParser::pushState()}. - * - * @param integer $state State to set - * @return integer - */ - public function setState($state) - { - $r = array_pop($this->states); - array_push($this->states, $state); - $this->state = $this->states[count($this->states) - 1]; - return $r; - } - /** - * Removes the exclusive state. - * - * @return void - */ - public function unsetExclusive() - { - $this->stateExclusive = false; - } - /** - * Removes the media types state. - * - * @return void - */ - public function unsetMediaTypes() - { - $this->stateMediaTypes = false; - } - } - -/** - * {@link aCssFromatter Formatter} returning the CSS source in {@link http://goo.gl/j4XdU OTBS indent style} (The One True Brace Style). - * - * @package CssMin/Formatter - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssOtbsFormatter extends aCssFormatter - { - /** - * Implements {@link aCssFormatter::__toString()}. - * - * @return string - */ - public function __toString() - { - $r = array(); - $level = 0; - for ($i = 0, $l = count($this->tokens); $i < $l; $i++) - { - $token = $this->tokens[$i]; - $class = get_class($token); - $indent = str_repeat($this->indent, $level); - if ($class === "CssCommentToken") - { - $lines = array_map("trim", explode("\n", $token->Comment)); - for ($ii = 0, $ll = count($lines); $ii < $ll; $ii++) - { - $r[] = $indent . (substr($lines[$ii], 0, 1) == "*" ? " " : "") . $lines[$ii]; - } - } - elseif ($class === "CssAtCharsetToken") - { - $r[] = $indent . "@charset " . $token->Charset . ";"; - } - elseif ($class === "CssAtFontFaceStartToken") - { - $r[] = $indent . "@font-face {"; - $level++; - } - elseif ($class === "CssAtImportToken") - { - $r[] = $indent . "@import " . $token->Import . " " . implode(", ", $token->MediaTypes) . ";"; - } - elseif ($class === "CssAtKeyframesStartToken") - { - $r[] = $indent . "@keyframes \"" . $token->Name . "\" {"; - $level++; - } - elseif ($class === "CssAtMediaStartToken") - { - $r[] = $indent . "@media " . implode(", ", $token->MediaTypes) . " {"; - $level++; - } - elseif ($class === "CssAtPageStartToken") - { - $r[] = $indent . "@page {"; - $level++; - } - elseif ($class === "CssAtVariablesStartToken") - { - $r[] = $indent . "@variables " . implode(", ", $token->MediaTypes) . " {"; - $level++; - } - elseif ($class === "CssRulesetStartToken" || $class === "CssAtKeyframesRulesetStartToken") - { - $r[] = $indent . implode(", ", $token->Selectors) . " {"; - $level++; - } - elseif ($class == "CssAtFontFaceDeclarationToken" - || $class === "CssAtKeyframesRulesetDeclarationToken" - || $class === "CssAtPageDeclarationToken" - || $class == "CssAtVariablesDeclarationToken" - || $class === "CssRulesetDeclarationToken" - ) - { - $declaration = $indent . $token->Property . ": "; - if ($this->padding) - { - $declaration = str_pad($declaration, $this->padding, " ", STR_PAD_RIGHT); - } - $r[] = $declaration . $token->Value . ($token->IsImportant ? " !important" : "") . ";"; - } - elseif ($class === "CssAtFontFaceEndToken" - || $class === "CssAtMediaEndToken" - || $class === "CssAtKeyframesEndToken" - || $class === "CssAtKeyframesRulesetEndToken" - || $class === "CssAtPageEndToken" - || $class === "CssAtVariablesEndToken" - || $class === "CssRulesetEndToken" - ) - { - $level--; - $r[] = str_repeat($indent, $level) . "}"; - } - } - return implode("\n", $r); - } - } - -/** - * This {@link aCssToken CSS token} is a utility token that extends {@link aNullToken} and returns only a empty string. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssNullToken extends aCssToken - { - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return ""; - } - } - -/** - * CSS Minifier. - * - * @package CssMin/Minifier - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssMinifier - { - /** - * {@link aCssMinifierFilter Filters}. - * - * @var array - */ - private $filters = array(); - /** - * {@link aCssMinifierPlugin Plugins}. - * - * @var array - */ - private $plugins = array(); - /** - * Minified source. - * - * @var string - */ - private $minified = ""; - /** - * Constructer. - * - * Creates instances of {@link aCssMinifierFilter filters} and {@link aCssMinifierPlugin plugins}. - * - * @param string $source CSS source [optional] - * @param array $filters Filter configuration [optional] - * @param array $plugins Plugin configuration [optional] - * @return void - */ - public function __construct($source = null, array $filters = null, array $plugins = null) - { - $filters = array_merge(array - ( - "ImportImports" => false, - "RemoveComments" => true, - "RemoveEmptyRulesets" => true, - "RemoveEmptyAtBlocks" => true, - "ConvertLevel3Properties" => false, - "ConvertLevel3AtKeyframes" => false, - "Variables" => true, - "RemoveLastDelarationSemiColon" => true - ), is_array($filters) ? $filters : array()); - $plugins = array_merge(array - ( - "Variables" => true, - "ConvertFontWeight" => false, - "ConvertHslColors" => false, - "ConvertRgbColors" => false, - "ConvertNamedColors" => false, - "CompressColorValues" => false, - "CompressUnitValues" => false, - "CompressExpressionValues" => false - ), is_array($plugins) ? $plugins : array()); - // Filters - foreach ($filters as $name => $config) - { - if ($config !== false) - { - $class = "Css" . $name . "MinifierFilter"; - $config = is_array($config) ? $config : array(); - if (class_exists($class)) - { - $this->filters[] = new $class($this, $config); - } - else - { - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": The filter " . $name . " with the class name " . $class . " was not found")); - } - } - } - // Plugins - foreach ($plugins as $name => $config) - { - if ($config !== false) - { - $class = "Css" . $name . "MinifierPlugin"; - $config = is_array($config) ? $config : array(); - if (class_exists($class)) - { - $this->plugins[] = new $class($this, $config); - } - else - { - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": The plugin " . $name . " with the class name " . $class . " was not found")); - } - } - } - // -- - if (!is_null($source)) - { - $this->minify($source); - } - } - /** - * Returns the minified Source. - * - * @return string - */ - public function getMinified() - { - return $this->minified; - } - /** - * Returns a plugin by class name. - * - * @param string $name Class name of the plugin - * @return aCssMinifierPlugin - */ - public function getPlugin($class) - { - static $index = null; - if (is_null($index)) - { - $index = array(); - for ($i = 0, $l = count($this->plugins); $i < $l; $i++) - { - $index[get_class($this->plugins[$i])] = $i; - } - } - return isset($index[$class]) ? $this->plugins[$index[$class]] : false; - } - /** - * Minifies the CSS source. - * - * @param string $source CSS source - * @return string - */ - public function minify($source) - { - // Variables - $r = ""; - $parser = new CssParser($source); - $tokens = $parser->getTokens(); - $filters = $this->filters; - $filterCount = count($this->filters); - $plugins = $this->plugins; - $pluginCount = count($plugins); - $pluginIndex = array(); - $pluginTriggerTokens = array(); - $globalTriggerTokens = array(); - for ($i = 0, $l = count($plugins); $i < $l; $i++) - { - $tPluginClassName = get_class($plugins[$i]); - $pluginTriggerTokens[$i] = $plugins[$i]->getTriggerTokens(); - foreach ($pluginTriggerTokens[$i] as $v) - { - if (!in_array($v, $globalTriggerTokens)) - { - $globalTriggerTokens[] = $v; - } - } - $pluginTriggerTokens[$i] = "|" . implode("|", $pluginTriggerTokens[$i]) . "|"; - $pluginIndex[$tPluginClassName] = $i; - } - $globalTriggerTokens = "|" . implode("|", $globalTriggerTokens) . "|"; - /* - * Apply filters - */ - for($i = 0; $i < $filterCount; $i++) - { - // Apply the filter; if the return value is larger than 0... - if ($filters[$i]->apply($tokens) > 0) - { - // ...then filter null values and rebuild the token array - $tokens = array_values(array_filter($tokens)); - } - } - $tokenCount = count($tokens); - /* - * Apply plugins - */ - for($i = 0; $i < $tokenCount; $i++) - { - $triggerToken = "|" . get_class($tokens[$i]) . "|"; - if (strpos($globalTriggerTokens, $triggerToken) !== false) - { - for($ii = 0; $ii < $pluginCount; $ii++) - { - if (strpos($pluginTriggerTokens[$ii], $triggerToken) !== false || $pluginTriggerTokens[$ii] === false) - { - // Apply the plugin; if the return value is TRUE continue to the next token - if ($plugins[$ii]->apply($tokens[$i]) === true) - { - continue 2; - } - } - } - } - } - // Stringify the tokens - for($i = 0; $i < $tokenCount; $i++) - { - $r .= (string) $tokens[$i]; - } - $this->minified = $r; - return $r; - } - } - -/** - * CssMin - A (simple) css minifier with benefits - * - * -- - * Copyright (c) 2011 Joe Scylla - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * -- - * - * @package CssMin - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssMin - { - /** - * Index of classes - * - * @var array - */ - private static $classIndex = array(); - /** - * Parse/minify errors - * - * @var array - */ - private static $errors = array(); - /** - * Verbose output. - * - * @var boolean - */ - private static $isVerbose = false; - /** - * {@link http://goo.gl/JrW54 Autoload} function of CssMin. - * - * @param string $class Name of the class - * @return void - */ - public static function autoload($class) - { - if (isset(self::$classIndex[$class])) - { - require(self::$classIndex[$class]); - } - } - /** - * Return errors - * - * @return array of {CssError}. - */ - public static function getErrors() - { - return self::$errors; - } - /** - * Returns if there were errors. - * - * @return boolean - */ - public static function hasErrors() - { - return count(self::$errors) > 0; - } - /** - * Initialises CssMin. - * - * @return void - */ - public static function initialise() - { - // Create the class index for autoloading or including - $paths = array(dirname(__FILE__)); - while (list($i, $path) = each($paths)) - { - $subDirectorys = glob($path . "*", GLOB_MARK | GLOB_ONLYDIR | GLOB_NOSORT); - if (is_array($subDirectorys)) - { - foreach ($subDirectorys as $subDirectory) - { - $paths[] = $subDirectory; - } - } - $files = glob($path . "*.php", 0); - if (is_array($files)) - { - foreach ($files as $file) - { - $class = substr(basename($file), 0, -4); - self::$classIndex[$class] = $file; - } - } - } - krsort(self::$classIndex); - // Only use autoloading if spl_autoload_register() is available and no __autoload() is defined (because - // __autoload() breaks if spl_autoload_register() is used. - if (function_exists("spl_autoload_register") && !is_callable("__autoload")) - { - spl_autoload_register(array(__CLASS__, "autoload")); - } - // Otherwise include all class files - else - { - foreach (self::$classIndex as $class => $file) - { - if (!class_exists($class)) - { - require_once($file); - } - } - } - } - /** - * Minifies CSS source. - * - * @param string $source CSS source - * @param array $filters Filter configuration [optional] - * @param array $plugins Plugin configuration [optional] - * @return string Minified CSS - */ - public static function minify($source, array $filters = null, array $plugins = null) - { - self::$errors = array(); - $minifier = new CssMinifier($source, $filters, $plugins); - return $minifier->getMinified(); - } - /** - * Parse the CSS source. - * - * @param string $source CSS source - * @param array $plugins Plugin configuration [optional] - * @return array Array of aCssToken - */ - public static function parse($source, array $plugins = null) - { - self::$errors = array(); - $parser = new CssParser($source, $plugins); - return $parser->getTokens(); - } - /** - * -- - * - * @param boolean $to - * @return boolean - */ - public static function setVerbose($to) - { - self::$isVerbose = (boolean) $to; - return self::$isVerbose; - } - /** - * -- - * - * @param CssError $error - * @return void - */ - public static function triggerError(CssError $error) - { - self::$errors[] = $error; - if (self::$isVerbose) - { - trigger_error((string) $error, E_USER_WARNING); - } - } - } -// Initialises CssMin -CssMin::initialise(); - -/** - * This {@link aCssMinifierFilter minifier filter} import external css files defined with the @import at-rule into the - * current stylesheet. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssImportImportsMinifierFilter extends aCssMinifierFilter - { - /** - * Array with already imported external stylesheets. - * - * @var array - */ - private $imported = array(); - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value large than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - if (!isset($this->configuration["BasePath"]) || !is_dir($this->configuration["BasePath"])) - { - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Base path " . ($this->configuration["BasePath"] ? $this->configuration["BasePath"] : "null"). " is not a directory")); - return 0; - } - for ($i = 0, $l = count($tokens); $i < $l; $i++) - { - if (get_class($tokens[$i]) === "CssAtImportToken") - { - $import = $this->configuration["BasePath"] . "/" . $tokens[$i]->Import; - // Import file was not found/is not a file - if (!is_file($import)) - { - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Import file " . $import. " was not found.", (string) $tokens[$i])); - } - // Import file already imported; remove this @import at-rule to prevent recursions - elseif (in_array($import, $this->imported)) - { - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Import file " . $import. " was already imported.", (string) $tokens[$i])); - $tokens[$i] = null; - } - else - { - $this->imported[] = $import; - $parser = new CssParser(file_get_contents($import)); - $import = $parser->getTokens(); - // The @import at-rule has media types defined requiring special handling - if (count($tokens[$i]->MediaTypes) > 0 && !(count($tokens[$i]->MediaTypes) == 1 && $tokens[$i]->MediaTypes[0] == "all")) - { - $blocks = array(); - /* - * Filter or set media types of @import at-rule or remove the @import at-rule if no media type is matching the parent @import at-rule - */ - for($ii = 0, $ll = count($import); $ii < $ll; $ii++) - { - if (get_class($import[$ii]) === "CssAtImportToken") - { - // @import at-rule defines no media type or only the "all" media type; set the media types to the one defined in the parent @import at-rule - if (count($import[$ii]->MediaTypes) == 0 || (count($import[$ii]->MediaTypes) == 1 && $import[$ii]->MediaTypes[0] == "all")) - { - $import[$ii]->MediaTypes = $tokens[$i]->MediaTypes; - } - // @import at-rule defineds one or more media types; filter out media types not matching with the parent @import at-rule - elseif (count($import[$ii]->MediaTypes) > 0) - { - foreach ($import[$ii]->MediaTypes as $index => $mediaType) - { - if (!in_array($mediaType, $tokens[$i]->MediaTypes)) - { - unset($import[$ii]->MediaTypes[$index]); - } - } - $import[$ii]->MediaTypes = array_values($import[$ii]->MediaTypes); - // If there are no media types left in the @import at-rule remove the @import at-rule - if (count($import[$ii]->MediaTypes) == 0) - { - $import[$ii] = null; - } - } - } - } - /* - * Remove media types of @media at-rule block not defined in the @import at-rule - */ - for($ii = 0, $ll = count($import); $ii < $ll; $ii++) - { - if (get_class($import[$ii]) === "CssAtMediaStartToken") - { - foreach ($import[$ii]->MediaTypes as $index => $mediaType) - { - if (!in_array($mediaType, $tokens[$i]->MediaTypes)) - { - unset($import[$ii]->MediaTypes[$index]); - } - $import[$ii]->MediaTypes = array_values($import[$ii]->MediaTypes); - } - } - } - /* - * If no media types left of the @media at-rule block remove the complete block - */ - for($ii = 0, $ll = count($import); $ii < $ll; $ii++) - { - if (get_class($import[$ii]) === "CssAtMediaStartToken") - { - if (count($import[$ii]->MediaTypes) === 0) - { - for ($iii = $ii; $iii < $ll; $iii++) - { - if (get_class($import[$iii]) === "CssAtMediaEndToken") - { - break; - } - } - if (get_class($import[$iii]) === "CssAtMediaEndToken") - { - array_splice($import, $ii, $iii - $ii + 1, array()); - $ll = count($import); - } - } - } - } - /* - * If the media types of the @media at-rule equals the media types defined in the @import - * at-rule remove the CssAtMediaStartToken and CssAtMediaEndToken token - */ - for($ii = 0, $ll = count($import); $ii < $ll; $ii++) - { - if (get_class($import[$ii]) === "CssAtMediaStartToken" && count(array_diff($tokens[$i]->MediaTypes, $import[$ii]->MediaTypes)) === 0) - { - for ($iii = $ii; $iii < $ll; $iii++) - { - if (get_class($import[$iii]) == "CssAtMediaEndToken") - { - break; - } - } - if (get_class($import[$iii]) == "CssAtMediaEndToken") - { - unset($import[$ii]); - unset($import[$iii]); - $import = array_values($import); - $ll = count($import); - } - } - } - /** - * Extract CssAtImportToken and CssAtCharsetToken tokens - */ - for($ii = 0, $ll = count($import); $ii < $ll; $ii++) - { - $class = get_class($import[$ii]); - if ($class === "CssAtImportToken" || $class === "CssAtCharsetToken") - { - $blocks = array_merge($blocks, array_splice($import, $ii, 1, array())); - $ll = count($import); - } - } - /* - * Extract the @font-face, @media and @page at-rule block - */ - for($ii = 0, $ll = count($import); $ii < $ll; $ii++) - { - $class = get_class($import[$ii]); - if ($class === "CssAtFontFaceStartToken" || $class === "CssAtMediaStartToken" || $class === "CssAtPageStartToken" || $class === "CssAtVariablesStartToken") - { - for ($iii = $ii; $iii < $ll; $iii++) - { - $class = get_class($import[$iii]); - if ($class === "CssAtFontFaceEndToken" || $class === "CssAtMediaEndToken" || $class === "CssAtPageEndToken" || $class === "CssAtVariablesEndToken") - { - break; - } - } - $class = get_class($import[$iii]); - if (isset($import[$iii]) && ($class === "CssAtFontFaceEndToken" || $class === "CssAtMediaEndToken" || $class === "CssAtPageEndToken" || $class === "CssAtVariablesEndToken")) - { - $blocks = array_merge($blocks, array_splice($import, $ii, $iii - $ii + 1, array())); - $ll = count($import); - } - } - } - // Create the import array with extracted tokens and the rulesets wrapped into a @media at-rule block - $import = array_merge($blocks, array(new CssAtMediaStartToken($tokens[$i]->MediaTypes)), $import, array(new CssAtMediaEndToken())); - } - // Insert the imported tokens - array_splice($tokens, $i, 1, $import); - // Modify parameters of the for-loop - $i--; - $l = count($tokens); - } - } - } - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for preserve parsing expression() declaration values. - * - * This plugin return no {@link aCssToken CssToken} but ensures that expression() declaration values will get parsed - * properly. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssExpressionParserPlugin extends aCssParserPlugin - { - /** - * Count of left braces. - * - * @var integer - */ - private $leftBraces = 0; - /** - * Count of right braces. - * - * @var integer - */ - private $rightBraces = 0; - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("(", ")", ";", "}"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return false; - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - // Start of expression - if ($char === "(" && strtolower(substr($this->parser->getSource(), $index - 10, 11)) === "expression(" && $state !== "T_EXPRESSION") - { - $this->parser->pushState("T_EXPRESSION"); - $this->leftBraces++; - } - // Count left braces - elseif ($char === "(" && $state === "T_EXPRESSION") - { - $this->leftBraces++; - } - // Count right braces - elseif ($char === ")" && $state === "T_EXPRESSION") - { - $this->rightBraces++; - } - // Possible end of expression; if left and right braces are equal the expressen ends - elseif (($char === ";" || $char === "}") && $state === "T_EXPRESSION" && $this->leftBraces === $this->rightBraces) - { - $this->leftBraces = $this->rightBraces = 0; - $this->parser->popState(); - return $index - 1; - } - else - { - return false; - } - return true; - } - } - -/** - * CSS Error. - * - * @package CssMin - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssError - { - /** - * File. - * - * @var string - */ - public $File = ""; - /** - * Line. - * - * @var integer - */ - public $Line = 0; - /** - * Error message. - * - * @var string - */ - public $Message = ""; - /** - * Source. - * - * @var string - */ - public $Source = ""; - /** - * Constructor triggering the error. - * - * @param string $message Error message - * @param string $source Corresponding line [optional] - * @return void - */ - public function __construct($file, $line, $message, $source = "") - { - $this->File = $file; - $this->Line = $line; - $this->Message = $message; - $this->Source = $source; - } - /** - * Returns the error as formatted string. - * - * @return string - */ - public function __toString() - { - return $this->Message . ($this->Source ? ":
    " . $this->Source . "": "") . "
    in file " . $this->File . " at line " . $this->Line; - } - } - -/** - * This {@link aCssMinifierPlugin} will convert a color value in rgb notation to hexadecimal notation. - * - * Example: - * - * color: rgb(200,60%,5); - * - * - * Will get converted to: - * - * color:#c89905; - * - * - * @package CssMin/Minifier/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssConvertRgbColorsMinifierPlugin extends aCssMinifierPlugin - { - /** - * Regular expression matching the value. - * - * @var string - */ - private $reMatch = "/rgb\s*\(\s*([0-9%]+)\s*,\s*([0-9%]+)\s*,\s*([0-9%]+)\s*\)/iS"; - /** - * Implements {@link aCssMinifierPlugin::minify()}. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - public function apply(aCssToken &$token) - { - if (stripos($token->Value, "rgb") !== false && preg_match($this->reMatch, $token->Value, $m)) - { - for ($i = 1, $l = count($m); $i < $l; $i++) - { - if (strpos("%", $m[$i]) !== false) - { - $m[$i] = substr($m[$i], 0, -1); - $m[$i] = (int) (256 * ($m[$i] / 100)); - } - $m[$i] = str_pad(dechex($m[$i]), 2, "0", STR_PAD_LEFT); - } - $token->Value = str_replace($m[0], "#" . $m[1] . $m[2] . $m[3], $token->Value); - } - return false; - } - /** - * Implements {@link aMinifierPlugin::getTriggerTokens()} - * - * @return array - */ - public function getTriggerTokens() - { - return array - ( - "CssAtFontFaceDeclarationToken", - "CssAtPageDeclarationToken", - "CssRulesetDeclarationToken" - ); - } - } - -/** - * This {@link aCssMinifierPlugin} will convert named color values to hexadecimal notation. - * - * Example: - * - * color: black; - * border: 1px solid indigo; - * - * - * Will get converted to: - * - * color:#000; - * border:1px solid #4b0082; - * - * - * @package CssMin/Minifier/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssConvertNamedColorsMinifierPlugin extends aCssMinifierPlugin - { - - /** - * Regular expression matching the value. - * - * @var string - */ - private $reMatch = null; - /** - * Regular expression replacing the value. - * - * @var string - */ - private $reReplace = "\"\${1}\" . \$this->transformation[strtolower(\"\${2}\")] . \"\${3}\""; - /** - * Transformation table used by the {@link CssConvertNamedColorsMinifierPlugin::$reReplace replace regular expression}. - * - * @var array - */ - private $transformation = array - ( - "aliceblue" => "#f0f8ff", - "antiquewhite" => "#faebd7", - "aqua" => "#0ff", - "aquamarine" => "#7fffd4", - "azure" => "#f0ffff", - "beige" => "#f5f5dc", - "black" => "#000", - "blue" => "#00f", - "blueviolet" => "#8a2be2", - "brown" => "#a52a2a", - "burlywood" => "#deb887", - "cadetblue" => "#5f9ea0", - "chartreuse" => "#7fff00", - "chocolate" => "#d2691e", - "coral" => "#ff7f50", - "cornflowerblue" => "#6495ed", - "cornsilk" => "#fff8dc", - "crimson" => "#dc143c", - "darkblue" => "#00008b", - "darkcyan" => "#008b8b", - "darkgoldenrod" => "#b8860b", - "darkgray" => "#a9a9a9", - "darkgreen" => "#006400", - "darkkhaki" => "#bdb76b", - "darkmagenta" => "#8b008b", - "darkolivegreen" => "#556b2f", - "darkorange" => "#ff8c00", - "darkorchid" => "#9932cc", - "darkred" => "#8b0000", - "darksalmon" => "#e9967a", - "darkseagreen" => "#8fbc8f", - "darkslateblue" => "#483d8b", - "darkslategray" => "#2f4f4f", - "darkturquoise" => "#00ced1", - "darkviolet" => "#9400d3", - "deeppink" => "#ff1493", - "deepskyblue" => "#00bfff", - "dimgray" => "#696969", - "dodgerblue" => "#1e90ff", - "firebrick" => "#b22222", - "floralwhite" => "#fffaf0", - "forestgreen" => "#228b22", - "fuchsia" => "#f0f", - "gainsboro" => "#dcdcdc", - "ghostwhite" => "#f8f8ff", - "gold" => "#ffd700", - "goldenrod" => "#daa520", - "gray" => "#808080", - "green" => "#008000", - "greenyellow" => "#adff2f", - "honeydew" => "#f0fff0", - "hotpink" => "#ff69b4", - "indianred" => "#cd5c5c", - "indigo" => "#4b0082", - "ivory" => "#fffff0", - "khaki" => "#f0e68c", - "lavender" => "#e6e6fa", - "lavenderblush" => "#fff0f5", - "lawngreen" => "#7cfc00", - "lemonchiffon" => "#fffacd", - "lightblue" => "#add8e6", - "lightcoral" => "#f08080", - "lightcyan" => "#e0ffff", - "lightgoldenrodyellow" => "#fafad2", - "lightgreen" => "#90ee90", - "lightgrey" => "#d3d3d3", - "lightpink" => "#ffb6c1", - "lightsalmon" => "#ffa07a", - "lightseagreen" => "#20b2aa", - "lightskyblue" => "#87cefa", - "lightslategray" => "#789", - "lightsteelblue" => "#b0c4de", - "lightyellow" => "#ffffe0", - "lime" => "#0f0", - "limegreen" => "#32cd32", - "linen" => "#faf0e6", - "maroon" => "#800000", - "mediumaquamarine" => "#66cdaa", - "mediumblue" => "#0000cd", - "mediumorchid" => "#ba55d3", - "mediumpurple" => "#9370db", - "mediumseagreen" => "#3cb371", - "mediumslateblue" => "#7b68ee", - "mediumspringgreen" => "#00fa9a", - "mediumturquoise" => "#48d1cc", - "mediumvioletred" => "#c71585", - "midnightblue" => "#191970", - "mintcream" => "#f5fffa", - "mistyrose" => "#ffe4e1", - "moccasin" => "#ffe4b5", - "navajowhite" => "#ffdead", - "navy" => "#000080", - "oldlace" => "#fdf5e6", - "olive" => "#808000", - "olivedrab" => "#6b8e23", - "orange" => "#ffa500", - "orangered" => "#ff4500", - "orchid" => "#da70d6", - "palegoldenrod" => "#eee8aa", - "palegreen" => "#98fb98", - "paleturquoise" => "#afeeee", - "palevioletred" => "#db7093", - "papayawhip" => "#ffefd5", - "peachpuff" => "#ffdab9", - "peru" => "#cd853f", - "pink" => "#ffc0cb", - "plum" => "#dda0dd", - "powderblue" => "#b0e0e6", - "purple" => "#800080", - "red" => "#f00", - "rosybrown" => "#bc8f8f", - "royalblue" => "#4169e1", - "saddlebrown" => "#8b4513", - "salmon" => "#fa8072", - "sandybrown" => "#f4a460", - "seagreen" => "#2e8b57", - "seashell" => "#fff5ee", - "sienna" => "#a0522d", - "silver" => "#c0c0c0", - "skyblue" => "#87ceeb", - "slateblue" => "#6a5acd", - "slategray" => "#708090", - "snow" => "#fffafa", - "springgreen" => "#00ff7f", - "steelblue" => "#4682b4", - "tan" => "#d2b48c", - "teal" => "#008080", - "thistle" => "#d8bfd8", - "tomato" => "#ff6347", - "turquoise" => "#40e0d0", - "violet" => "#ee82ee", - "wheat" => "#f5deb3", - "white" => "#fff", - "whitesmoke" => "#f5f5f5", - "yellow" => "#ff0", - "yellowgreen" => "#9acd32" - ); - /** - * Overwrites {@link aCssMinifierPlugin::__construct()}. - * - * The constructor will create the {@link CssConvertNamedColorsMinifierPlugin::$reReplace replace regular expression} - * based on the {@link CssConvertNamedColorsMinifierPlugin::$transformation transformation table}. - * - * @param CssMinifier $minifier The CssMinifier object of this plugin. - * @param array $configuration Plugin configuration [optional] - * @return void - */ - public function __construct(CssMinifier $minifier, array $configuration = array()) - { - $this->reMatch = "/(^|\s)+(" . implode("|", array_keys($this->transformation)) . ")(\s|$)+/eiS"; - parent::__construct($minifier, $configuration); - } - /** - * Implements {@link aCssMinifierPlugin::minify()}. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - public function apply(aCssToken &$token) - { - $lcValue = strtolower($token->Value); - // Declaration value equals a value in the transformation table => simple replace - if (isset($this->transformation[$lcValue])) - { - $token->Value = $this->transformation[$lcValue]; - } - // Declaration value contains a value in the transformation table => regular expression replace - elseif (preg_match($this->reMatch, $token->Value)) - { - $token->Value = preg_replace($this->reMatch, $this->reReplace, $token->Value); - } - return false; - } - /** - * Implements {@link aMinifierPlugin::getTriggerTokens()} - * - * @return array - */ - public function getTriggerTokens() - { - return array - ( - "CssAtFontFaceDeclarationToken", - "CssAtPageDeclarationToken", - "CssRulesetDeclarationToken" - ); - } - } - -/** - * This {@link aCssMinifierFilter minifier filter} triggers on CSS Level 3 properties and will add declaration tokens - * with browser-specific properties. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssConvertLevel3PropertiesMinifierFilter extends aCssMinifierFilter - { - /** - * Css property transformations table. Used to convert CSS3 and proprietary properties to the browser-specific - * counterparts. - * - * @var array - */ - private $transformations = array - ( - // Property Array(Mozilla, Webkit, Opera, Internet Explorer); NULL values are placeholders and will get ignored - "animation" => array(null, "-webkit-animation", null, null), - "animation-delay" => array(null, "-webkit-animation-delay", null, null), - "animation-direction" => array(null, "-webkit-animation-direction", null, null), - "animation-duration" => array(null, "-webkit-animation-duration", null, null), - "animation-fill-mode" => array(null, "-webkit-animation-fill-mode", null, null), - "animation-iteration-count" => array(null, "-webkit-animation-iteration-count", null, null), - "animation-name" => array(null, "-webkit-animation-name", null, null), - "animation-play-state" => array(null, "-webkit-animation-play-state", null, null), - "animation-timing-function" => array(null, "-webkit-animation-timing-function", null, null), - "appearance" => array("-moz-appearance", "-webkit-appearance", null, null), - "backface-visibility" => array(null, "-webkit-backface-visibility", null, null), - "background-clip" => array(null, "-webkit-background-clip", null, null), - "background-composite" => array(null, "-webkit-background-composite", null, null), - "background-inline-policy" => array("-moz-background-inline-policy", null, null, null), - "background-origin" => array(null, "-webkit-background-origin", null, null), - "background-position-x" => array(null, null, null, "-ms-background-position-x"), - "background-position-y" => array(null, null, null, "-ms-background-position-y"), - "background-size" => array(null, "-webkit-background-size", null, null), - "behavior" => array(null, null, null, "-ms-behavior"), - "binding" => array("-moz-binding", null, null, null), - "border-after" => array(null, "-webkit-border-after", null, null), - "border-after-color" => array(null, "-webkit-border-after-color", null, null), - "border-after-style" => array(null, "-webkit-border-after-style", null, null), - "border-after-width" => array(null, "-webkit-border-after-width", null, null), - "border-before" => array(null, "-webkit-border-before", null, null), - "border-before-color" => array(null, "-webkit-border-before-color", null, null), - "border-before-style" => array(null, "-webkit-border-before-style", null, null), - "border-before-width" => array(null, "-webkit-border-before-width", null, null), - "border-border-bottom-colors" => array("-moz-border-bottom-colors", null, null, null), - "border-bottom-left-radius" => array("-moz-border-radius-bottomleft", "-webkit-border-bottom-left-radius", null, null), - "border-bottom-right-radius" => array("-moz-border-radius-bottomright", "-webkit-border-bottom-right-radius", null, null), - "border-end" => array("-moz-border-end", "-webkit-border-end", null, null), - "border-end-color" => array("-moz-border-end-color", "-webkit-border-end-color", null, null), - "border-end-style" => array("-moz-border-end-style", "-webkit-border-end-style", null, null), - "border-end-width" => array("-moz-border-end-width", "-webkit-border-end-width", null, null), - "border-fit" => array(null, "-webkit-border-fit", null, null), - "border-horizontal-spacing" => array(null, "-webkit-border-horizontal-spacing", null, null), - "border-image" => array("-moz-border-image", "-webkit-border-image", null, null), - "border-left-colors" => array("-moz-border-left-colors", null, null, null), - "border-radius" => array("-moz-border-radius", "-webkit-border-radius", null, null), - "border-border-right-colors" => array("-moz-border-right-colors", null, null, null), - "border-start" => array("-moz-border-start", "-webkit-border-start", null, null), - "border-start-color" => array("-moz-border-start-color", "-webkit-border-start-color", null, null), - "border-start-style" => array("-moz-border-start-style", "-webkit-border-start-style", null, null), - "border-start-width" => array("-moz-border-start-width", "-webkit-border-start-width", null, null), - "border-top-colors" => array("-moz-border-top-colors", null, null, null), - "border-top-left-radius" => array("-moz-border-radius-topleft", "-webkit-border-top-left-radius", null, null), - "border-top-right-radius" => array("-moz-border-radius-topright", "-webkit-border-top-right-radius", null, null), - "border-vertical-spacing" => array(null, "-webkit-border-vertical-spacing", null, null), - "box-align" => array("-moz-box-align", "-webkit-box-align", null, null), - "box-direction" => array("-moz-box-direction", "-webkit-box-direction", null, null), - "box-flex" => array("-moz-box-flex", "-webkit-box-flex", null, null), - "box-flex-group" => array(null, "-webkit-box-flex-group", null, null), - "box-flex-lines" => array(null, "-webkit-box-flex-lines", null, null), - "box-ordinal-group" => array("-moz-box-ordinal-group", "-webkit-box-ordinal-group", null, null), - "box-orient" => array("-moz-box-orient", "-webkit-box-orient", null, null), - "box-pack" => array("-moz-box-pack", "-webkit-box-pack", null, null), - "box-reflect" => array(null, "-webkit-box-reflect", null, null), - "box-shadow" => array("-moz-box-shadow", "-webkit-box-shadow", null, null), - "box-sizing" => array("-moz-box-sizing", null, null, null), - "color-correction" => array(null, "-webkit-color-correction", null, null), - "column-break-after" => array(null, "-webkit-column-break-after", null, null), - "column-break-before" => array(null, "-webkit-column-break-before", null, null), - "column-break-inside" => array(null, "-webkit-column-break-inside", null, null), - "column-count" => array("-moz-column-count", "-webkit-column-count", null, null), - "column-gap" => array("-moz-column-gap", "-webkit-column-gap", null, null), - "column-rule" => array("-moz-column-rule", "-webkit-column-rule", null, null), - "column-rule-color" => array("-moz-column-rule-color", "-webkit-column-rule-color", null, null), - "column-rule-style" => array("-moz-column-rule-style", "-webkit-column-rule-style", null, null), - "column-rule-width" => array("-moz-column-rule-width", "-webkit-column-rule-width", null, null), - "column-span" => array(null, "-webkit-column-span", null, null), - "column-width" => array("-moz-column-width", "-webkit-column-width", null, null), - "columns" => array(null, "-webkit-columns", null, null), - "filter" => array(__CLASS__, "filter"), - "float-edge" => array("-moz-float-edge", null, null, null), - "font-feature-settings" => array("-moz-font-feature-settings", null, null, null), - "font-language-override" => array("-moz-font-language-override", null, null, null), - "font-size-delta" => array(null, "-webkit-font-size-delta", null, null), - "font-smoothing" => array(null, "-webkit-font-smoothing", null, null), - "force-broken-image-icon" => array("-moz-force-broken-image-icon", null, null, null), - "highlight" => array(null, "-webkit-highlight", null, null), - "hyphenate-character" => array(null, "-webkit-hyphenate-character", null, null), - "hyphenate-locale" => array(null, "-webkit-hyphenate-locale", null, null), - "hyphens" => array(null, "-webkit-hyphens", null, null), - "force-broken-image-icon" => array("-moz-image-region", null, null, null), - "ime-mode" => array(null, null, null, "-ms-ime-mode"), - "interpolation-mode" => array(null, null, null, "-ms-interpolation-mode"), - "layout-flow" => array(null, null, null, "-ms-layout-flow"), - "layout-grid" => array(null, null, null, "-ms-layout-grid"), - "layout-grid-char" => array(null, null, null, "-ms-layout-grid-char"), - "layout-grid-line" => array(null, null, null, "-ms-layout-grid-line"), - "layout-grid-mode" => array(null, null, null, "-ms-layout-grid-mode"), - "layout-grid-type" => array(null, null, null, "-ms-layout-grid-type"), - "line-break" => array(null, "-webkit-line-break", null, "-ms-line-break"), - "line-clamp" => array(null, "-webkit-line-clamp", null, null), - "line-grid-mode" => array(null, null, null, "-ms-line-grid-mode"), - "logical-height" => array(null, "-webkit-logical-height", null, null), - "logical-width" => array(null, "-webkit-logical-width", null, null), - "margin-after" => array(null, "-webkit-margin-after", null, null), - "margin-after-collapse" => array(null, "-webkit-margin-after-collapse", null, null), - "margin-before" => array(null, "-webkit-margin-before", null, null), - "margin-before-collapse" => array(null, "-webkit-margin-before-collapse", null, null), - "margin-bottom-collapse" => array(null, "-webkit-margin-bottom-collapse", null, null), - "margin-collapse" => array(null, "-webkit-margin-collapse", null, null), - "margin-end" => array("-moz-margin-end", "-webkit-margin-end", null, null), - "margin-start" => array("-moz-margin-start", "-webkit-margin-start", null, null), - "margin-top-collapse" => array(null, "-webkit-margin-top-collapse", null, null), - "marquee " => array(null, "-webkit-marquee", null, null), - "marquee-direction" => array(null, "-webkit-marquee-direction", null, null), - "marquee-increment" => array(null, "-webkit-marquee-increment", null, null), - "marquee-repetition" => array(null, "-webkit-marquee-repetition", null, null), - "marquee-speed" => array(null, "-webkit-marquee-speed", null, null), - "marquee-style" => array(null, "-webkit-marquee-style", null, null), - "mask" => array(null, "-webkit-mask", null, null), - "mask-attachment" => array(null, "-webkit-mask-attachment", null, null), - "mask-box-image" => array(null, "-webkit-mask-box-image", null, null), - "mask-clip" => array(null, "-webkit-mask-clip", null, null), - "mask-composite" => array(null, "-webkit-mask-composite", null, null), - "mask-image" => array(null, "-webkit-mask-image", null, null), - "mask-origin" => array(null, "-webkit-mask-origin", null, null), - "mask-position" => array(null, "-webkit-mask-position", null, null), - "mask-position-x" => array(null, "-webkit-mask-position-x", null, null), - "mask-position-y" => array(null, "-webkit-mask-position-y", null, null), - "mask-repeat" => array(null, "-webkit-mask-repeat", null, null), - "mask-repeat-x" => array(null, "-webkit-mask-repeat-x", null, null), - "mask-repeat-y" => array(null, "-webkit-mask-repeat-y", null, null), - "mask-size" => array(null, "-webkit-mask-size", null, null), - "match-nearest-mail-blockquote-color" => array(null, "-webkit-match-nearest-mail-blockquote-color", null, null), - "max-logical-height" => array(null, "-webkit-max-logical-height", null, null), - "max-logical-width" => array(null, "-webkit-max-logical-width", null, null), - "min-logical-height" => array(null, "-webkit-min-logical-height", null, null), - "min-logical-width" => array(null, "-webkit-min-logical-width", null, null), - "object-fit" => array(null, null, "-o-object-fit", null), - "object-position" => array(null, null, "-o-object-position", null), - "opacity" => array(__CLASS__, "opacity"), - "outline-radius" => array("-moz-outline-radius", null, null, null), - "outline-bottom-left-radius" => array("-moz-outline-radius-bottomleft", null, null, null), - "outline-bottom-right-radius" => array("-moz-outline-radius-bottomright", null, null, null), - "outline-top-left-radius" => array("-moz-outline-radius-topleft", null, null, null), - "outline-top-right-radius" => array("-moz-outline-radius-topright", null, null, null), - "padding-after" => array(null, "-webkit-padding-after", null, null), - "padding-before" => array(null, "-webkit-padding-before", null, null), - "padding-end" => array("-moz-padding-end", "-webkit-padding-end", null, null), - "padding-start" => array("-moz-padding-start", "-webkit-padding-start", null, null), - "perspective" => array(null, "-webkit-perspective", null, null), - "perspective-origin" => array(null, "-webkit-perspective-origin", null, null), - "perspective-origin-x" => array(null, "-webkit-perspective-origin-x", null, null), - "perspective-origin-y" => array(null, "-webkit-perspective-origin-y", null, null), - "rtl-ordering" => array(null, "-webkit-rtl-ordering", null, null), - "scrollbar-3dlight-color" => array(null, null, null, "-ms-scrollbar-3dlight-color"), - "scrollbar-arrow-color" => array(null, null, null, "-ms-scrollbar-arrow-color"), - "scrollbar-base-color" => array(null, null, null, "-ms-scrollbar-base-color"), - "scrollbar-darkshadow-color" => array(null, null, null, "-ms-scrollbar-darkshadow-color"), - "scrollbar-face-color" => array(null, null, null, "-ms-scrollbar-face-color"), - "scrollbar-highlight-color" => array(null, null, null, "-ms-scrollbar-highlight-color"), - "scrollbar-shadow-color" => array(null, null, null, "-ms-scrollbar-shadow-color"), - "scrollbar-track-color" => array(null, null, null, "-ms-scrollbar-track-color"), - "stack-sizing" => array("-moz-stack-sizing", null, null, null), - "svg-shadow" => array(null, "-webkit-svg-shadow", null, null), - "tab-size" => array("-moz-tab-size", null, "-o-tab-size", null), - "table-baseline" => array(null, null, "-o-table-baseline", null), - "text-align-last" => array(null, null, null, "-ms-text-align-last"), - "text-autospace" => array(null, null, null, "-ms-text-autospace"), - "text-combine" => array(null, "-webkit-text-combine", null, null), - "text-decorations-in-effect" => array(null, "-webkit-text-decorations-in-effect", null, null), - "text-emphasis" => array(null, "-webkit-text-emphasis", null, null), - "text-emphasis-color" => array(null, "-webkit-text-emphasis-color", null, null), - "text-emphasis-position" => array(null, "-webkit-text-emphasis-position", null, null), - "text-emphasis-style" => array(null, "-webkit-text-emphasis-style", null, null), - "text-fill-color" => array(null, "-webkit-text-fill-color", null, null), - "text-justify" => array(null, null, null, "-ms-text-justify"), - "text-kashida-space" => array(null, null, null, "-ms-text-kashida-space"), - "text-overflow" => array(null, null, "-o-text-overflow", "-ms-text-overflow"), - "text-security" => array(null, "-webkit-text-security", null, null), - "text-size-adjust" => array(null, "-webkit-text-size-adjust", null, "-ms-text-size-adjust"), - "text-stroke" => array(null, "-webkit-text-stroke", null, null), - "text-stroke-color" => array(null, "-webkit-text-stroke-color", null, null), - "text-stroke-width" => array(null, "-webkit-text-stroke-width", null, null), - "text-underline-position" => array(null, null, null, "-ms-text-underline-position"), - "transform" => array("-moz-transform", "-webkit-transform", "-o-transform", null), - "transform-origin" => array("-moz-transform-origin", "-webkit-transform-origin", "-o-transform-origin", null), - "transform-origin-x" => array(null, "-webkit-transform-origin-x", null, null), - "transform-origin-y" => array(null, "-webkit-transform-origin-y", null, null), - "transform-origin-z" => array(null, "-webkit-transform-origin-z", null, null), - "transform-style" => array(null, "-webkit-transform-style", null, null), - "transition" => array("-moz-transition", "-webkit-transition", "-o-transition", null), - "transition-delay" => array("-moz-transition-delay", "-webkit-transition-delay", "-o-transition-delay", null), - "transition-duration" => array("-moz-transition-duration", "-webkit-transition-duration", "-o-transition-duration", null), - "transition-property" => array("-moz-transition-property", "-webkit-transition-property", "-o-transition-property", null), - "transition-timing-function" => array("-moz-transition-timing-function", "-webkit-transition-timing-function", "-o-transition-timing-function", null), - "user-drag" => array(null, "-webkit-user-drag", null, null), - "user-focus" => array("-moz-user-focus", null, null, null), - "user-input" => array("-moz-user-input", null, null, null), - "user-modify" => array("-moz-user-modify", "-webkit-user-modify", null, null), - "user-select" => array("-moz-user-select", "-webkit-user-select", null, null), - "white-space" => array(__CLASS__, "whiteSpace"), - "window-shadow" => array("-moz-window-shadow", null, null, null), - "word-break" => array(null, null, null, "-ms-word-break"), - "word-wrap" => array(null, null, null, "-ms-word-wrap"), - "writing-mode" => array(null, "-webkit-writing-mode", null, "-ms-writing-mode"), - "zoom" => array(null, null, null, "-ms-zoom") - ); - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value large than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - $r = 0; - $transformations = &$this->transformations; - for ($i = 0, $l = count($tokens); $i < $l; $i++) - { - if (get_class($tokens[$i]) === "CssRulesetDeclarationToken") - { - $tProperty = $tokens[$i]->Property; - if (isset($transformations[$tProperty])) - { - $result = array(); - if (is_callable($transformations[$tProperty])) - { - $result = call_user_func_array($transformations[$tProperty], array($tokens[$i])); - if (!is_array($result) && is_object($result)) - { - $result = array($result); - } - } - else - { - $tValue = $tokens[$i]->Value; - $tMediaTypes = $tokens[$i]->MediaTypes; - foreach ($transformations[$tProperty] as $property) - { - if ($property !== null) - { - $result[] = new CssRulesetDeclarationToken($property, $tValue, $tMediaTypes); - } - } - } - if (count($result) > 0) - { - array_splice($tokens, $i + 1, 0, $result); - $i += count($result); - $l += count($result); - } - } - } - } - return $r; - } - /** - * Transforms the Internet Explorer specific declaration property "filter" to Internet Explorer 8+ compatible - * declaratiopn property "-ms-filter". - * - * @param aCssToken $token - * @return array - */ - private static function filter($token) - { - $r = array - ( - new CssRulesetDeclarationToken("-ms-filter", "\"" . $token->Value . "\"", $token->MediaTypes), - ); - return $r; - } - /** - * Transforms "opacity: {value}" into browser specific counterparts. - * - * @param aCssToken $token - * @return array - */ - private static function opacity($token) - { - // Calculate the value for Internet Explorer filter statement - $ieValue = (int) ((float) $token->Value * 100); - $r = array - ( - // Internet Explorer >= 8 - new CssRulesetDeclarationToken("-ms-filter", "\"alpha(opacity=" . $ieValue . ")\"", $token->MediaTypes), - // Internet Explorer >= 4 <= 7 - new CssRulesetDeclarationToken("filter", "alpha(opacity=" . $ieValue . ")", $token->MediaTypes), - new CssRulesetDeclarationToken("zoom", "1", $token->MediaTypes) - ); - return $r; - } - /** - * Transforms "white-space: pre-wrap" into browser specific counterparts. - * - * @param aCssToken $token - * @return array - */ - private static function whiteSpace($token) - { - if (strtolower($token->Value) === "pre-wrap") - { - $r = array - ( - // Firefox < 3 - new CssRulesetDeclarationToken("white-space", "-moz-pre-wrap", $token->MediaTypes), - // Webkit - new CssRulesetDeclarationToken("white-space", "-webkit-pre-wrap", $token->MediaTypes), - // Opera >= 4 <= 6 - new CssRulesetDeclarationToken("white-space", "-pre-wrap", $token->MediaTypes), - // Opera >= 7 - new CssRulesetDeclarationToken("white-space", "-o-pre-wrap", $token->MediaTypes), - // Internet Explorer >= 5.5 - new CssRulesetDeclarationToken("word-wrap", "break-word", $token->MediaTypes) - ); - return $r; - } - else - { - return array(); - } - } - } - -/** - * This {@link aCssMinifierFilter minifier filter} will convert @keyframes at-rule block to browser specific counterparts. - * - * @package CssMin/Minifier/Filters - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssConvertLevel3AtKeyframesMinifierFilter extends aCssMinifierFilter - { - /** - * Implements {@link aCssMinifierFilter::filter()}. - * - * @param array $tokens Array of objects of type aCssToken - * @return integer Count of added, changed or removed tokens; a return value larger than 0 will rebuild the array - */ - public function apply(array &$tokens) - { - $r = 0; - $transformations = array("-moz-keyframes", "-webkit-keyframes"); - for ($i = 0, $l = count($tokens); $i < $l; $i++) - { - if (get_class($tokens[$i]) === "CssAtKeyframesStartToken") - { - for ($ii = $i; $ii < $l; $ii++) - { - if (get_class($tokens[$ii]) === "CssAtKeyframesEndToken") - { - break; - } - } - if (get_class($tokens[$ii]) === "CssAtKeyframesEndToken") - { - $add = array(); - $source = array(); - for ($iii = $i; $iii <= $ii; $iii++) - { - $source[] = clone($tokens[$iii]); - } - foreach ($transformations as $transformation) - { - $t = array(); - foreach ($source as $token) - { - $t[] = clone($token); - } - $t[0]->AtRuleName = $transformation; - $add = array_merge($add, $t); - } - if (isset($this->configuration["RemoveSource"]) && $this->configuration["RemoveSource"] === true) - { - array_splice($tokens, $i, $ii - $i + 1, $add); - } - else - { - array_splice($tokens, $ii + 1, 0, $add); - } - $l = count($tokens); - $i = $ii + count($add); - $r += count($add); - } - } - } - return $r; - } - } - -/** - * This {@link aCssMinifierPlugin} will convert a color value in hsl notation to hexadecimal notation. - * - * Example: - * - * color: hsl(232,36%,48%); - * - * - * Will get converted to: - * - * color:#4e5aa7; - * - * - * @package CssMin/Minifier/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssConvertHslColorsMinifierPlugin extends aCssMinifierPlugin - { - /** - * Regular expression matching the value. - * - * @var string - */ - private $reMatch = "/^hsl\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*%\s*,\s*([0-9]+)\s*%\s*\)/iS"; - /** - * Implements {@link aCssMinifierPlugin::minify()}. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - public function apply(aCssToken &$token) - { - if (stripos($token->Value, "hsl") !== false && preg_match($this->reMatch, $token->Value, $m)) - { - $token->Value = str_replace($m[0], $this->hsl2hex($m[1], $m[2], $m[3]), $token->Value); - } - return false; - } - /** - * Implements {@link aMinifierPlugin::getTriggerTokens()} - * - * @return array - */ - public function getTriggerTokens() - { - return array - ( - "CssAtFontFaceDeclarationToken", - "CssAtPageDeclarationToken", - "CssRulesetDeclarationToken" - ); - } - /** - * Convert a HSL value to hexadecimal notation. - * - * Based on: {@link http://www.easyrgb.com/index.php?X=MATH&H=19#text19}. - * - * @param integer $hue Hue - * @param integer $saturation Saturation - * @param integer $lightness Lightnesss - * @return string - */ - private function hsl2hex($hue, $saturation, $lightness) - { - $hue = $hue / 360; - $saturation = $saturation / 100; - $lightness = $lightness / 100; - if ($saturation == 0) - { - $red = $lightness * 255; - $green = $lightness * 255; - $blue = $lightness * 255; - } - else - { - if ($lightness < 0.5 ) - { - $v2 = $lightness * (1 + $saturation); - } - else - { - $v2 = ($lightness + $saturation) - ($saturation * $lightness); - } - $v1 = 2 * $lightness - $v2; - $red = 255 * self::hue2rgb($v1, $v2, $hue + (1 / 3)); - $green = 255 * self::hue2rgb($v1, $v2, $hue); - $blue = 255 * self::hue2rgb($v1, $v2, $hue - (1 / 3)); - } - return "#" . str_pad(dechex(round($red)), 2, "0", STR_PAD_LEFT) . str_pad(dechex(round($green)), 2, "0", STR_PAD_LEFT) . str_pad(dechex(round($blue)), 2, "0", STR_PAD_LEFT); - } - /** - * Apply hue to a rgb color value. - * - * @param integer $v1 Value 1 - * @param integer $v2 Value 2 - * @param integer $hue Hue - * @return integer - */ - private function hue2rgb($v1, $v2, $hue) - { - if ($hue < 0) - { - $hue += 1; - } - if ($hue > 1) - { - $hue -= 1; - } - if ((6 * $hue) < 1) - { - return ($v1 + ($v2 - $v1) * 6 * $hue); - } - if ((2 * $hue) < 1) - { - return ($v2); - } - if ((3 * $hue) < 2) - { - return ($v1 + ($v2 - $v1) * (( 2 / 3) - $hue) * 6); - } - return $v1; - } - } - -/** - * This {@link aCssMinifierPlugin} will convert the font-weight values normal and bold to their numeric notation. - * - * Example: - * - * font-weight: normal; - * font: bold 11px monospace; - * - * - * Will get converted to: - * - * font-weight:400; - * font:700 11px monospace; - * - * - * @package CssMin/Minifier/Pluginsn - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssConvertFontWeightMinifierPlugin extends aCssMinifierPlugin - { - /** - * Array of included declaration properties this plugin will process; others declaration properties will get - * ignored. - * - * @var array - */ - private $include = array - ( - "font", - "font-weight" - ); - /** - * Regular expression matching the value. - * - * @var string - */ - private $reMatch = null; - /** - * Regular expression replace the value. - * - * @var string - */ - private $reReplace = "\"\${1}\" . \$this->transformation[\"\${2}\"] . \"\${3}\""; - /** - * Transformation table used by the {@link CssConvertFontWeightMinifierPlugin::$reReplace replace regular expression}. - * - * @var array - */ - private $transformation = array - ( - "normal" => "400", - "bold" => "700" - ); - /** - * Overwrites {@link aCssMinifierPlugin::__construct()}. - * - * The constructor will create the {@link CssConvertFontWeightMinifierPlugin::$reReplace replace regular expression} - * based on the {@link CssConvertFontWeightMinifierPlugin::$transformation transformation table}. - * - * @param CssMinifier $minifier The CssMinifier object of this plugin. - * @return void - */ - public function __construct(CssMinifier $minifier) - { - $this->reMatch = "/(^|\s)+(" . implode("|", array_keys($this->transformation)). ")(\s|$)+/eiS"; - parent::__construct($minifier); - } - /** - * Implements {@link aCssMinifierPlugin::minify()}. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - public function apply(aCssToken &$token) - { - if (in_array($token->Property, $this->include) && preg_match($this->reMatch, $token->Value, $m)) - { - $token->Value = preg_replace($this->reMatch, $this->reReplace, $token->Value); - } - return false; - } - /** - * Implements {@link aMinifierPlugin::getTriggerTokens()} - * - * @return array - */ - public function getTriggerTokens() - { - return array - ( - "CssAtFontFaceDeclarationToken", - "CssAtPageDeclarationToken", - "CssRulesetDeclarationToken" - ); - } - } - -/** - * This {@link aCssMinifierPlugin} will compress several unit values to their short notations. Examples: - * - * - * padding: 0.5em; - * border: 0px; - * margin: 0 0 0 0; - * - * - * Will get compressed to: - * - * - * padding:.5px; - * border:0; - * margin:0; - * - * - * -- - * - * @package CssMin/Minifier/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssCompressUnitValuesMinifierPlugin extends aCssMinifierPlugin - { - /** - * Regular expression used for matching and replacing unit values. - * - * @var array - */ - private $re = array - ( - "/(^| |-)0\.([0-9]+?)(0+)?(%|em|ex|px|in|cm|mm|pt|pc)/iS" => "\${1}.\${2}\${4}", - "/(^| )-?(\.?)0(%|em|ex|px|in|cm|mm|pt|pc)/iS" => "\${1}0", - "/(^0\s0\s0\s0)|(^0\s0\s0$)|(^0\s0$)/iS" => "0" - ); - /** - * Regular expression matching the value. - * - * @var string - */ - private $reMatch = "/(^| |-)0\.([0-9]+?)(0+)?(%|em|ex|px|in|cm|mm|pt|pc)|(^| )-?(\.?)0(%|em|ex|px|in|cm|mm|pt|pc)|(^0\s0\s0\s0$)|(^0\s0\s0$)|(^0\s0$)/iS"; - /** - * Implements {@link aCssMinifierPlugin::minify()}. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - public function apply(aCssToken &$token) - { - if (preg_match($this->reMatch, $token->Value)) - { - foreach ($this->re as $reMatch => $reReplace) - { - $token->Value = preg_replace($reMatch, $reReplace, $token->Value); - } - } - return false; - } - /** - * Implements {@link aMinifierPlugin::getTriggerTokens()} - * - * @return array - */ - public function getTriggerTokens() - { - return array - ( - "CssAtFontFaceDeclarationToken", - "CssAtPageDeclarationToken", - "CssRulesetDeclarationToken" - ); - } - } - -/** - * This {@link aCssMinifierPlugin} compress the content of expresssion() declaration values. - * - * For compression of expressions {@link https://github.com/rgrove/jsmin-php/ JSMin} will get used. JSMin have to be - * already included or loadable via {@link http://goo.gl/JrW54 PHP autoloading}. - * - * @package CssMin/Minifier/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssCompressExpressionValuesMinifierPlugin extends aCssMinifierPlugin - { - /** - * Implements {@link aCssMinifierPlugin::minify()}. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - public function apply(aCssToken &$token) - { - if (class_exists("JSMin") && stripos($token->Value, "expression(") !== false) - { - $value = $token->Value; - $value = substr($token->Value, stripos($token->Value, "expression(") + 10); - $value = trim(JSMin::minify($value)); - $token->Value = "expression(" . $value . ")"; - } - return false; - } - /** - * Implements {@link aMinifierPlugin::getTriggerTokens()} - * - * @return array - */ - public function getTriggerTokens() - { - return array - ( - "CssAtFontFaceDeclarationToken", - "CssAtPageDeclarationToken", - "CssRulesetDeclarationToken" - ); - } - } - -/** - * This {@link aCssMinifierPlugin} will convert hexadecimal color value with 6 chars to their 3 char hexadecimal - * notation (if possible). - * - * Example: - * - * color: #aabbcc; - * - * - * Will get converted to: - * - * color:#abc; - * - * - * @package CssMin/Minifier/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssCompressColorValuesMinifierPlugin extends aCssMinifierPlugin - { - /** - * Regular expression matching 6 char hexadecimal color values. - * - * @var string - */ - private $reMatch = "/\#([0-9a-f]{6})/iS"; - /** - * Implements {@link aCssMinifierPlugin::minify()}. - * - * @param aCssToken $token Token to process - * @return boolean Return TRUE to break the processing of this token; FALSE to continue - */ - public function apply(aCssToken &$token) - { - if (strpos($token->Value, "#") !== false && preg_match($this->reMatch, $token->Value, $m)) - { - $value = strtolower($m[1]); - if ($value[0] == $value[1] && $value[2] == $value[3] && $value[4] == $value[5]) - { - $token->Value = str_replace($m[0], "#" . $value[0] . $value[2] . $value[4], $token->Value); - } - } - return false; - } - /** - * Implements {@link aMinifierPlugin::getTriggerTokens()} - * - * @return array - */ - public function getTriggerTokens() - { - return array - ( - "CssAtFontFaceDeclarationToken", - "CssAtPageDeclarationToken", - "CssRulesetDeclarationToken" - ); - } - } - -/** - * This {@link aCssToken CSS token} represents a CSS comment. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssCommentToken extends aCssToken - { - /** - * Comment as Text. - * - * @var string - */ - public $Comment = ""; - /** - * Set the properties of a comment token. - * - * @param string $comment Comment including comment delimiters - * @return void - */ - public function __construct($comment) - { - $this->Comment = $comment; - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return $this->Comment; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing comments. - * - * Adds a {@link CssCommentToken} to the parser if a comment was found. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssCommentParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("*", "/"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return false; - } - /** - * Stored buffer for restore. - * - * @var string - */ - private $restoreBuffer = ""; - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - if ($char === "*" && $previousChar === "/" && $state !== "T_COMMENT") - { - $this->parser->pushState("T_COMMENT"); - $this->parser->setExclusive(__CLASS__); - $this->restoreBuffer = substr($this->parser->getAndClearBuffer(), 0, -2); - } - elseif ($char === "/" && $previousChar === "*" && $state === "T_COMMENT") - { - $this->parser->popState(); - $this->parser->unsetExclusive(); - $this->parser->appendToken(new CssCommentToken("/*" . $this->parser->getAndClearBuffer())); - $this->parser->setBuffer($this->restoreBuffer); - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssToken CSS token} represents the start of a @variables at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtVariablesStartToken extends aCssAtBlockStartToken - { - /** - * Media types of the @variables at-rule block. - * - * @var array - */ - public $MediaTypes = array(); - /** - * Set the properties of a @variables at-rule token. - * - * @param array $mediaTypes Media types - * @return void - */ - public function __construct($mediaTypes = null) - { - $this->MediaTypes = $mediaTypes ? $mediaTypes : array("all"); - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return ""; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing @variables at-rule block with including declarations. - * - * Found @variables at-rule blocks will add a {@link CssAtVariablesStartToken} and {@link CssAtVariablesEndToken} to the - * parser; including declarations as {@link CssAtVariablesDeclarationToken}. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtVariablesParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("@", "{", "}", ":", ";"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return array("T_DOCUMENT", "T_AT_VARIABLES::PREPARE", "T_AT_VARIABLES", "T_AT_VARIABLES_DECLARATION"); - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - // Start of @variables at-rule block - if ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 10)) === "@variables") - { - $this->parser->pushState("T_AT_VARIABLES::PREPARE"); - $this->parser->clearBuffer(); - return $index + 10; - } - // Start of @variables declarations - elseif ($char === "{" && $state === "T_AT_VARIABLES::PREPARE") - { - $this->parser->setState("T_AT_VARIABLES"); - $mediaTypes = array_filter(array_map("trim", explode(",", $this->parser->getAndClearBuffer("{")))); - $this->parser->appendToken(new CssAtVariablesStartToken($mediaTypes)); - } - // Start of @variables declaration - if ($char === ":" && $state === "T_AT_VARIABLES") - { - $this->buffer = $this->parser->getAndClearBuffer(":"); - $this->parser->pushState("T_AT_VARIABLES_DECLARATION"); - } - // Unterminated @variables declaration - elseif ($char === ":" && $state === "T_AT_VARIABLES_DECLARATION") - { - // Ignore Internet Explorer filter declarations - if ($this->buffer === "filter") - { - return false; - } - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Unterminated @variables declaration", $this->buffer . ":" . $this->parser->getBuffer() . "_")); - } - // End of @variables declaration - elseif (($char === ";" || $char === "}") && $state === "T_AT_VARIABLES_DECLARATION") - { - $value = $this->parser->getAndClearBuffer(";}"); - if (strtolower(substr($value, -10, 10)) === "!important") - { - $value = trim(substr($value, 0, -10)); - $isImportant = true; - } - else - { - $isImportant = false; - } - $this->parser->popState(); - $this->parser->appendToken(new CssAtVariablesDeclarationToken($this->buffer, $value, $isImportant)); - $this->buffer = ""; - } - // End of @variables at-rule block - elseif ($char === "}" && $state === "T_AT_VARIABLES") - { - $this->parser->popState(); - $this->parser->clearBuffer(); - $this->parser->appendToken(new CssAtVariablesEndToken()); - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssToken CSS token} represents the end of a @variables at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtVariablesEndToken extends aCssAtBlockEndToken - { - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return ""; - } - } - -/** - * This {@link aCssToken CSS token} represents a declaration of a @variables at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtVariablesDeclarationToken extends aCssDeclarationToken - { - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return ""; - } - } - -/** -* This {@link aCssToken CSS token} represents the start of a @page at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtPageStartToken extends aCssAtBlockStartToken - { - /** - * Selector. - * - * @var string - */ - public $Selector = ""; - /** - * Sets the properties of the @page at-rule. - * - * @param string $selector Selector - * @return void - */ - public function __construct($selector = "") - { - $this->Selector = $selector; - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return "@page" . ($this->Selector ? " " . $this->Selector : "") . "{"; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing @page at-rule block with including declarations. - * - * Found @page at-rule blocks will add a {@link CssAtPageStartToken} and {@link CssAtPageEndToken} to the - * parser; including declarations as {@link CssAtPageDeclarationToken}. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtPageParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("@", "{", "}", ":", ";"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return array("T_DOCUMENT", "T_AT_PAGE::SELECTOR", "T_AT_PAGE", "T_AT_PAGE_DECLARATION"); - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - // Start of @page at-rule block - if ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 5)) === "@page") - { - $this->parser->pushState("T_AT_PAGE::SELECTOR"); - $this->parser->clearBuffer(); - return $index + 5; - } - // Start of @page declarations - elseif ($char === "{" && $state === "T_AT_PAGE::SELECTOR") - { - $selector = $this->parser->getAndClearBuffer("{"); - $this->parser->setState("T_AT_PAGE"); - $this->parser->clearBuffer(); - $this->parser->appendToken(new CssAtPageStartToken($selector)); - } - // Start of @page declaration - elseif ($char === ":" && $state === "T_AT_PAGE") - { - $this->parser->pushState("T_AT_PAGE_DECLARATION"); - $this->buffer = $this->parser->getAndClearBuffer(":", true); - } - // Unterminated @font-face declaration - elseif ($char === ":" && $state === "T_AT_PAGE_DECLARATION") - { - // Ignore Internet Explorer filter declarations - if ($this->buffer === "filter") - { - return false; - } - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Unterminated @page declaration", $this->buffer . ":" . $this->parser->getBuffer() . "_")); - } - // End of @page declaration - elseif (($char === ";" || $char === "}") && $state == "T_AT_PAGE_DECLARATION") - { - $value = $this->parser->getAndClearBuffer(";}"); - if (strtolower(substr($value, -10, 10)) == "!important") - { - $value = trim(substr($value, 0, -10)); - $isImportant = true; - } - else - { - $isImportant = false; - } - $this->parser->popState(); - $this->parser->appendToken(new CssAtPageDeclarationToken($this->buffer, $value, $isImportant)); - // -- - if ($char === "}") - { - $this->parser->popState(); - $this->parser->appendToken(new CssAtPageEndToken()); - } - $this->buffer = ""; - } - // End of @page at-rule block - elseif ($char === "}" && $state === "T_AT_PAGE") - { - $this->parser->popState(); - $this->parser->clearBuffer(); - $this->parser->appendToken(new CssAtPageEndToken()); - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssToken CSS token} represents the end of a @page at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtPageEndToken extends aCssAtBlockEndToken - { - - } - -/** - * This {@link aCssToken CSS token} represents a declaration of a @page at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtPageDeclarationToken extends aCssDeclarationToken - { - - } - -/** - * This {@link aCssToken CSS token} represents the start of a @media at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtMediaStartToken extends aCssAtBlockStartToken - { - /** - * Sets the properties of the @media at-rule. - * - * @param array $mediaTypes Media types - * @return void - */ - public function __construct(array $mediaTypes = array()) - { - $this->MediaTypes = $mediaTypes; - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return "@media " . implode(",", $this->MediaTypes) . "{"; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing @media at-rule block. - * - * Found @media at-rule blocks will add a {@link CssAtMediaStartToken} and {@link CssAtMediaEndToken} to the parser. - * This plugin will also set the the current media types using {@link CssParser::setMediaTypes()} and - * {@link CssParser::unsetMediaTypes()}. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtMediaParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("@", "{", "}"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return array("T_DOCUMENT", "T_AT_MEDIA::PREPARE", "T_AT_MEDIA"); - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - if ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 6)) === "@media") - { - $this->parser->pushState("T_AT_MEDIA::PREPARE"); - $this->parser->clearBuffer(); - return $index + 6; - } - elseif ($char === "{" && $state === "T_AT_MEDIA::PREPARE") - { - $mediaTypes = array_filter(array_map("trim", explode(",", $this->parser->getAndClearBuffer("{")))); - $this->parser->setMediaTypes($mediaTypes); - $this->parser->setState("T_AT_MEDIA"); - $this->parser->appendToken(new CssAtMediaStartToken($mediaTypes)); - } - elseif ($char === "}" && $state === "T_AT_MEDIA") - { - $this->parser->appendToken(new CssAtMediaEndToken()); - $this->parser->clearBuffer(); - $this->parser->unsetMediaTypes(); - $this->parser->popState(); - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssToken CSS token} represents the end of a @media at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtMediaEndToken extends aCssAtBlockEndToken - { - - } - -/** - * This {@link aCssToken CSS token} represents the start of a @keyframes at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtKeyframesStartToken extends aCssAtBlockStartToken - { - /** - * Name of the at-rule. - * - * @var string - */ - public $AtRuleName = "keyframes"; - /** - * Name - * - * @var string - */ - public $Name = ""; - /** - * Sets the properties of the @page at-rule. - * - * @param string $selector Selector - * @return void - */ - public function __construct($name, $atRuleName = null) - { - $this->Name = $name; - if (!is_null($atRuleName)) - { - $this->AtRuleName = $atRuleName; - } - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return "@" . $this->AtRuleName . " \"" . $this->Name . "\"{"; - } - } - -/** - * This {@link aCssToken CSS token} represents the start of a ruleset of a @keyframes at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtKeyframesRulesetStartToken extends aCssRulesetStartToken - { - /** - * Array of selectors. - * - * @var array - */ - public $Selectors = array(); - /** - * Set the properties of a ruleset token. - * - * @param array $selectors Selectors of the ruleset - * @return void - */ - public function __construct(array $selectors = array()) - { - $this->Selectors = $selectors; - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return implode(",", $this->Selectors) . "{"; - } - } - -/** - * This {@link aCssToken CSS token} represents the end of a ruleset of a @keyframes at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtKeyframesRulesetEndToken extends aCssRulesetEndToken - { - - } - -/** - * This {@link aCssToken CSS token} represents a ruleset declaration of a @keyframes at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtKeyframesRulesetDeclarationToken extends aCssDeclarationToken - { - - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing @keyframes at-rule blocks, rulesets and declarations. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtKeyframesParserPlugin extends aCssParserPlugin - { - /** - * @var string Keyword - */ - private $atRuleName = ""; - /** - * Selectors. - * - * @var array - */ - private $selectors = array(); - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("@", "{", "}", ":", ",", ";"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return array("T_DOCUMENT", "T_AT_KEYFRAMES::NAME", "T_AT_KEYFRAMES", "T_AT_KEYFRAMES_RULESETS", "T_AT_KEYFRAMES_RULESET", "T_AT_KEYFRAMES_RULESET_DECLARATION"); - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - // Start of @keyframes at-rule block - if ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 10)) === "@keyframes") - { - $this->atRuleName = "keyframes"; - $this->parser->pushState("T_AT_KEYFRAMES::NAME"); - $this->parser->clearBuffer(); - return $index + 10; - } - // Start of @keyframes at-rule block (@-moz-keyframes) - elseif ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 15)) === "@-moz-keyframes") - { - $this->atRuleName = "-moz-keyframes"; - $this->parser->pushState("T_AT_KEYFRAMES::NAME"); - $this->parser->clearBuffer(); - return $index + 15; - } - // Start of @keyframes at-rule block (@-webkit-keyframes) - elseif ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 18)) === "@-webkit-keyframes") - { - $this->atRuleName = "-webkit-keyframes"; - $this->parser->pushState("T_AT_KEYFRAMES::NAME"); - $this->parser->clearBuffer(); - return $index + 18; - } - // Start of @keyframes rulesets - elseif ($char === "{" && $state === "T_AT_KEYFRAMES::NAME") - { - $name = $this->parser->getAndClearBuffer("{\"'"); - $this->parser->setState("T_AT_KEYFRAMES_RULESETS"); - $this->parser->clearBuffer(); - $this->parser->appendToken(new CssAtKeyframesStartToken($name, $this->atRuleName)); - } - // Start of @keyframe ruleset and selectors - if ($char === "," && $state === "T_AT_KEYFRAMES_RULESETS") - { - $this->selectors[] = $this->parser->getAndClearBuffer(",{"); - } - // Start of a @keyframes ruleset - elseif ($char === "{" && $state === "T_AT_KEYFRAMES_RULESETS") - { - if ($this->parser->getBuffer() !== "") - { - $this->selectors[] = $this->parser->getAndClearBuffer(",{"); - $this->parser->pushState("T_AT_KEYFRAMES_RULESET"); - $this->parser->appendToken(new CssAtKeyframesRulesetStartToken($this->selectors)); - $this->selectors = array(); - } - } - // Start of @keyframes ruleset declaration - elseif ($char === ":" && $state === "T_AT_KEYFRAMES_RULESET") - { - $this->parser->pushState("T_AT_KEYFRAMES_RULESET_DECLARATION"); - $this->buffer = $this->parser->getAndClearBuffer(":;", true); - } - // Unterminated @keyframes ruleset declaration - elseif ($char === ":" && $state === "T_AT_KEYFRAMES_RULESET_DECLARATION") - { - // Ignore Internet Explorer filter declarations - if ($this->buffer === "filter") - { - return false; - } - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Unterminated @keyframes ruleset declaration", $this->buffer . ":" . $this->parser->getBuffer() . "_")); - } - // End of declaration - elseif (($char === ";" || $char === "}") && $state === "T_AT_KEYFRAMES_RULESET_DECLARATION") - { - $value = $this->parser->getAndClearBuffer(";}"); - if (strtolower(substr($value, -10, 10)) === "!important") - { - $value = trim(substr($value, 0, -10)); - $isImportant = true; - } - else - { - $isImportant = false; - } - $this->parser->popState(); - $this->parser->appendToken(new CssAtKeyframesRulesetDeclarationToken($this->buffer, $value, $isImportant)); - // Declaration ends with a right curly brace; so we have to end the ruleset - if ($char === "}") - { - $this->parser->appendToken(new CssAtKeyframesRulesetEndToken()); - $this->parser->popState(); - } - $this->buffer = ""; - } - // End of @keyframes ruleset - elseif ($char === "}" && $state === "T_AT_KEYFRAMES_RULESET") - { - $this->parser->clearBuffer(); - - $this->parser->popState(); - $this->parser->appendToken(new CssAtKeyframesRulesetEndToken()); - } - // End of @keyframes rulesets - elseif ($char === "}" && $state === "T_AT_KEYFRAMES_RULESETS") - { - $this->parser->clearBuffer(); - $this->parser->popState(); - $this->parser->appendToken(new CssAtKeyframesEndToken()); - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssToken CSS token} represents the end of a @keyframes at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtKeyframesEndToken extends aCssAtBlockEndToken - { - - } - -/** - * This {@link aCssToken CSS token} represents a @import at-rule. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1.b1 (2001-02-22) - */ -class CssAtImportToken extends aCssToken - { - /** - * Import path of the @import at-rule. - * - * @var string - */ - public $Import = ""; - /** - * Media types of the @import at-rule. - * - * @var array - */ - public $MediaTypes = array(); - /** - * Set the properties of a @import at-rule token. - * - * @param string $import Import path - * @param array $mediaTypes Media types - * @return void - */ - public function __construct($import, $mediaTypes) - { - $this->Import = $import; - $this->MediaTypes = $mediaTypes ? $mediaTypes : array(); - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return "@import \"" . $this->Import . "\"" . (count($this->MediaTypes) > 0 ? " " . implode(",", $this->MediaTypes) : ""). ";"; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing @import at-rule. - * - * If a @import at-rule was found this plugin will add a {@link CssAtImportToken} to the parser. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtImportParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("@", ";", ",", "\n"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return array("T_DOCUMENT", "T_AT_IMPORT"); - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - if ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 7)) === "@import") - { - $this->parser->pushState("T_AT_IMPORT"); - $this->parser->clearBuffer(); - return $index + 7; - } - elseif (($char === ";" || $char === "\n") && $state === "T_AT_IMPORT") - { - $this->buffer = $this->parser->getAndClearBuffer(";"); - $pos = false; - foreach (array(")", "\"", "'") as $needle) - { - if (($pos = strrpos($this->buffer, $needle)) !== false) - { - break; - } - } - $import = substr($this->buffer, 0, $pos + 1); - if (stripos($import, "url(") === 0) - { - $import = substr($import, 4, -1); - } - $import = trim($import, " \t\n\r\0\x0B'\""); - $mediaTypes = array_filter(array_map("trim", explode(",", trim(substr($this->buffer, $pos + 1), " \t\n\r\0\x0B{")))); - if ($pos) - { - $this->parser->appendToken(new CssAtImportToken($import, $mediaTypes)); - } - else - { - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Invalid @import at-rule syntax", $this->parser->buffer)); - } - $this->parser->popState(); - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssToken CSS token} represents the start of a @font-face at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtFontFaceStartToken extends aCssAtBlockStartToken - { - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return "@font-face{"; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing @font-face at-rule block with including declarations. - * - * Found @font-face at-rule blocks will add a {@link CssAtFontFaceStartToken} and {@link CssAtFontFaceEndToken} to the - * parser; including declarations as {@link CssAtFontFaceDeclarationToken}. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtFontFaceParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("@", "{", "}", ":", ";"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return array("T_DOCUMENT", "T_AT_FONT_FACE::PREPARE", "T_AT_FONT_FACE", "T_AT_FONT_FACE_DECLARATION"); - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - // Start of @font-face at-rule block - if ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 10)) === "@font-face") - { - $this->parser->pushState("T_AT_FONT_FACE::PREPARE"); - $this->parser->clearBuffer(); - return $index + 10; - } - // Start of @font-face declarations - elseif ($char === "{" && $state === "T_AT_FONT_FACE::PREPARE") - { - $this->parser->setState("T_AT_FONT_FACE"); - $this->parser->clearBuffer(); - $this->parser->appendToken(new CssAtFontFaceStartToken()); - } - // Start of @font-face declaration - elseif ($char === ":" && $state === "T_AT_FONT_FACE") - { - $this->parser->pushState("T_AT_FONT_FACE_DECLARATION"); - $this->buffer = $this->parser->getAndClearBuffer(":", true); - } - // Unterminated @font-face declaration - elseif ($char === ":" && $state === "T_AT_FONT_FACE_DECLARATION") - { - // Ignore Internet Explorer filter declarations - if ($this->buffer === "filter") - { - return false; - } - CssMin::triggerError(new CssError(__FILE__, __LINE__, __METHOD__ . ": Unterminated @font-face declaration", $this->buffer . ":" . $this->parser->getBuffer() . "_")); - } - // End of @font-face declaration - elseif (($char === ";" || $char === "}") && $state === "T_AT_FONT_FACE_DECLARATION") - { - $value = $this->parser->getAndClearBuffer(";}"); - if (strtolower(substr($value, -10, 10)) === "!important") - { - $value = trim(substr($value, 0, -10)); - $isImportant = true; - } - else - { - $isImportant = false; - } - $this->parser->popState(); - $this->parser->appendToken(new CssAtFontFaceDeclarationToken($this->buffer, $value, $isImportant)); - $this->buffer = ""; - // -- - if ($char === "}") - { - $this->parser->appendToken(new CssAtFontFaceEndToken()); - $this->parser->popState(); - } - } - // End of @font-face at-rule block - elseif ($char === "}" && $state === "T_AT_FONT_FACE") - { - $this->parser->appendToken(new CssAtFontFaceEndToken()); - $this->parser->clearBuffer(); - $this->parser->popState(); - } - else - { - return false; - } - return true; - } - } - -/** - * This {@link aCssToken CSS token} represents the end of a @font-face at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtFontFaceEndToken extends aCssAtBlockEndToken - { - - } - -/** - * This {@link aCssToken CSS token} represents a declaration of a @font-face at-rule block. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtFontFaceDeclarationToken extends aCssDeclarationToken - { - - } - -/** - * This {@link aCssToken CSS token} represents a @charset at-rule. - * - * @package CssMin/Tokens - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtCharsetToken extends aCssToken - { - /** - * Charset of the @charset at-rule. - * - * @var string - */ - public $Charset = ""; - /** - * Set the properties of @charset at-rule token. - * - * @param string $charset Charset of the @charset at-rule token - * @return void - */ - public function __construct($charset) - { - $this->Charset = $charset; - } - /** - * Implements {@link aCssToken::__toString()}. - * - * @return string - */ - public function __toString() - { - return "@charset " . $this->Charset . ";"; - } - } - -/** - * {@link aCssParserPlugin Parser plugin} for parsing @charset at-rule. - * - * If a @charset at-rule was found this plugin will add a {@link CssAtCharsetToken} to the parser. - * - * @package CssMin/Parser/Plugins - * @link http://code.google.com/p/cssmin/ - * @author Joe Scylla - * @copyright 2008 - 2011 Joe Scylla - * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 3.0.1 - */ -class CssAtCharsetParserPlugin extends aCssParserPlugin - { - /** - * Implements {@link aCssParserPlugin::getTriggerChars()}. - * - * @return array - */ - public function getTriggerChars() - { - return array("@", ";", "\n"); - } - /** - * Implements {@link aCssParserPlugin::getTriggerStates()}. - * - * @return array - */ - public function getTriggerStates() - { - return array("T_DOCUMENT", "T_AT_CHARSET"); - } - /** - * Implements {@link aCssParserPlugin::parse()}. - * - * @param integer $index Current index - * @param string $char Current char - * @param string $previousChar Previous char - * @return mixed TRUE will break the processing; FALSE continue with the next plugin; integer set a new index and break the processing - */ - public function parse($index, $char, $previousChar, $state) - { - if ($char === "@" && $state === "T_DOCUMENT" && strtolower(substr($this->parser->getSource(), $index, 8)) === "@charset") - { - $this->parser->pushState("T_AT_CHARSET"); - $this->parser->clearBuffer(); - return $index + 8; - } - elseif (($char === ";" || $char === "\n") && $state === "T_AT_CHARSET") - { - $charset = $this->parser->getAndClearBuffer(";"); - $this->parser->popState(); - $this->parser->appendToken(new CssAtCharsetToken($charset)); - } - else - { - return false; - } - return true; - } - } - -?> \ No newline at end of file diff --git a/includes/libraries/class-emogrifier.php b/includes/libraries/class-emogrifier.php new file mode 100644 index 00000000000..490c2586ce2 --- /dev/null +++ b/includes/libraries/class-emogrifier.php @@ -0,0 +1,1555 @@ + + * @author Roman Ožana + * @author Sander Kruger + */ +// @codingStandardsIgnoreFile +class Emogrifier +{ + /** + * @var int + */ + const CACHE_KEY_CSS = 0; + + /** + * @var int + */ + const CACHE_KEY_SELECTOR = 1; + + /** + * @var int + */ + const CACHE_KEY_XPATH = 2; + + /** + * @var int + */ + const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 3; + + /** + * @var int + */ + const CACHE_KEY_COMBINED_STYLES = 4; + + /** + * for calculating nth-of-type and nth-child selectors + * + * @var int + */ + const INDEX = 0; + + /** + * for calculating nth-of-type and nth-child selectors + * + * @var int + */ + const MULTIPLIER = 1; + + /** + * @var string + */ + const ID_ATTRIBUTE_MATCHER = '/(\\w+)?\\#([\\w\\-]+)/'; + + /** + * @var string + */ + const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/'; + + /** + * @var string + */ + const CONTENT_TYPE_META_TAG = ''; + + /** + * @var string + */ + const DEFAULT_DOCUMENT_TYPE = ''; + + /** + * @var string + */ + private $html = ''; + + /** + * @var string + */ + private $css = ''; + + /** + * @var bool[] + */ + private $excludedSelectors = array(); + + /** + * @var string[] + */ + private $unprocessableHtmlTags = array( 'wbr' ); + + /** + * @var bool[] + */ + private $allowedMediaTypes = array( 'all' => true, 'screen' => true, 'print' => true ); + + /** + * @var mixed[] + */ + private $caches = array( + self::CACHE_KEY_CSS => array(), + self::CACHE_KEY_SELECTOR => array(), + self::CACHE_KEY_XPATH => array(), + self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => array(), + self::CACHE_KEY_COMBINED_STYLES => array(), + ); + + /** + * the visited nodes with the XPath paths as array keys + * + * @var \DOMElement[] + */ + private $visitedNodes = array(); + + /** + * the styles to apply to the nodes with the XPath paths as array keys for the outer array + * and the attribute names/values as key/value pairs for the inner array + * + * @var string[][] + */ + private $styleAttributesForNodes = array(); + + /** + * Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved. + * If set to false, the value of the style attributes will be discarded. + * + * @var bool + */ + private $isInlineStyleAttributesParsingEnabled = true; + + /** + * Determines whether the + post_type ) || ! in_array( $post->post_type, array( 'product', 'product_variation' ) ) ) + if ( empty( $post->post_type ) || ! in_array( $post->post_type, array( 'product', 'product_variation' ) ) ) { return; + } - $GLOBALS['product'] = get_product( $post ); + $GLOBALS['product'] = wc_get_product( $post ); return $GLOBALS['product']; } @@ -92,24 +150,22 @@ if ( ! function_exists( 'woocommerce_reset_loop' ) ) { /** * Reset the loop's index and columns when we're done outputting a product loop. - * - * @access public * @subpackage Loop - * @return void */ function woocommerce_reset_loop() { - global $woocommerce_loop; - // Reset loop/columns globals when starting a new loop - $woocommerce_loop['loop'] = $woocommerce_loop['columns'] = ''; + $GLOBALS['woocommerce_loop'] = array( + 'loop' => '', + 'columns' => '', + 'name' => '', + ); } } add_filter( 'loop_end', 'woocommerce_reset_loop' ); /** * Products RSS Feed. - * + * @deprecated 2.6 * @access public - * @return void */ function wc_products_rss_feed() { // Product RSS @@ -117,24 +173,24 @@ function wc_products_rss_feed() { $feed = get_post_type_archive_feed_link( 'product' ); - echo ''; + echo ''; } elseif ( is_tax( 'product_cat' ) ) { - $term = get_term_by('slug', esc_attr( get_query_var('product_cat') ), 'product_cat'); - - $feed = add_query_arg('product_cat', $term->slug, get_post_type_archive_feed_link( 'product' )); - - echo ''; + $term = get_term_by( 'slug', esc_attr( get_query_var( 'product_cat' ) ), 'product_cat' ); + if ( $term ) { + $feed = add_query_arg( 'product_cat', $term->slug, get_post_type_archive_feed_link( 'product' ) ); + echo ''; + } } elseif ( is_tax( 'product_tag' ) ) { - $term = get_term_by('slug', esc_attr( get_query_var('product_tag') ), 'product_tag'); - - $feed = add_query_arg('product_tag', $term->slug, get_post_type_archive_feed_link( 'product' )); - - echo ''; + $term = get_term_by( 'slug', esc_attr( get_query_var( 'product_tag' ) ), 'product_tag' ); + if ( $term ) { + $feed = add_query_arg( 'product_tag', $term->slug, get_post_type_archive_feed_link( 'product' ) ); + echo ''; + } } } @@ -142,7 +198,11 @@ function wc_products_rss_feed() { * Output generator tag to aid debugging. * * @access public - * @return void + * + * @param string $gen + * @param string $type + * + * @return string */ function wc_generator_tag( $gen, $type ) { switch ( $type ) { @@ -157,7 +217,7 @@ function wc_generator_tag( $gen, $type ) { } /** - * Add body classes for WC pages + * Add body classes for WC pages. * * @param array $classes * @return array @@ -166,34 +226,94 @@ function wc_body_class( $classes ) { $classes = (array) $classes; if ( is_woocommerce() ) { + $classes[] = 'woocommerce'; $classes[] = 'woocommerce-page'; - } - elseif ( is_checkout() ) { + } elseif ( is_checkout() ) { + $classes[] = 'woocommerce-checkout'; $classes[] = 'woocommerce-page'; - } - elseif ( is_cart() ) { + } elseif ( is_cart() ) { + $classes[] = 'woocommerce-cart'; $classes[] = 'woocommerce-page'; - } - elseif ( is_account_page() ) { + } elseif ( is_account_page() ) { + $classes[] = 'woocommerce-account'; $classes[] = 'woocommerce-page'; + } if ( is_store_notice_showing() ) { $classes[] = 'woocommerce-demo-store'; } + foreach ( WC()->query->query_vars as $key => $value ) { + if ( is_wc_endpoint_url( $key ) ) { + $classes[] = 'woocommerce-' . sanitize_html_class( $key ); + } + } + return array_unique( $classes ); } /** - * Adds extra post classes for products + * Display the classes for the product cat div. + * + * @since 2.4.0 + * @param string|array $class One or more classes to add to the class list. + * @param object $category object Optional. + */ +function wc_product_cat_class( $class = '', $category = null ) { + // Separates classes with a single space, collates classes for post DIV + echo 'class="' . esc_attr( join( ' ', wc_get_product_cat_class( $class, $category ) ) ) . '"'; +} + +/** + * Get classname for loops based on $woocommerce_loop global. + * @since 2.6.0 + * @return string + */ +function wc_get_loop_class() { + global $woocommerce_loop; + + $woocommerce_loop['loop'] = ! empty( $woocommerce_loop['loop'] ) ? $woocommerce_loop['loop'] + 1 : 1; + $woocommerce_loop['columns'] = max( 1, ! empty( $woocommerce_loop['columns'] ) ? $woocommerce_loop['columns'] : apply_filters( 'loop_shop_columns', 4 ) ); + + if ( 0 === ( $woocommerce_loop['loop'] - 1 ) % $woocommerce_loop['columns'] || 1 === $woocommerce_loop['columns'] ) { + return 'first'; + } elseif ( 0 === $woocommerce_loop['loop'] % $woocommerce_loop['columns'] ) { + return 'last'; + } else { + return ''; + } +} + +/** + * Get the classes for the product cat div. + * + * @since 2.4.0 + * + * @param string|array $class One or more classes to add to the class list. + * @param object $category object Optional. + * + * @return array + */ +function wc_get_product_cat_class( $class = '', $category = null ) { + $classes = is_array( $class ) ? $class : array_map( 'trim', explode( ' ', $class ) ); + $classes[] = 'product-category'; + $classes[] = 'product'; + $classes[] = wc_get_loop_class(); + $classes = apply_filters( 'product_cat_class', $classes, $class, $category ); + + return array_unique( array_filter( $classes ) ); +} + +/** + * Adds extra post classes for products. * * @since 2.1.0 * @param array $classes @@ -202,12 +322,17 @@ function wc_body_class( $classes ) { * @return array */ function wc_product_post_class( $classes, $class = '', $post_id = '' ) { - if ( ! $post_id || get_post_type( $post_id ) !== 'product' ) + if ( ! $post_id || ! in_array( get_post_type( $post_id ), array( 'product', 'product_variation' ) ) ) { return $classes; + } - $product = get_product( $post_id ); + $product = wc_get_product( $post_id ); if ( $product ) { + $classes[] = 'product'; + $classes[] = wc_get_loop_class(); + $classes[] = $product->get_stock_status(); + if ( $product->is_on_sale() ) { $classes[] = 'sale'; } @@ -232,36 +357,63 @@ function wc_product_post_class( $classes, $class = '', $post_id = '' ) { if ( $product->is_purchasable() ) { $classes[] = 'purchasable'; } - if ( isset( $product->product_type ) ) { - $classes[] = "product-type-" . $product->product_type; + if ( $product->get_type() ) { + $classes[] = "product-type-" . $product->get_type(); } - - // add category slugs - $categories = wp_get_post_terms( $product->id, "product_cat" ); - if ( ! empty( $categories ) ) { - foreach ($categories as $key => $value) { - $classes[] = "product-cat-" . $value->slug; + if ( $product->is_type( 'variable' ) ) { + if ( ! $product->get_default_attributes() ) { + $classes[] = 'has-default-attributes'; + } + if ( $product->has_child() ) { + $classes[] = 'has-children'; } } - - // add tag slugs - $tags = wp_get_post_terms( $product->id, "product_tag" ); - if ( ! empty( $tags ) ) { - foreach ($tags as $key => $value) { - $classes[] = "product-tag-" . $value->slug; - } - } - - $classes[] = $product->stock_status; } - if ( ( $key = array_search( 'hentry', $classes ) ) !== false ) { + if ( false !== ( $key = array_search( 'hentry', $classes ) ) ) { unset( $classes[ $key ] ); } return $classes; } +/** + * Outputs hidden form inputs for each query string variable. + * @since 3.0.0 + * + * @param array $values Name value pairs. + * @param array $exclude Keys to exclude. + * @param string $current_key Current key we are outputting. + * @param bool $return + * @return string + */ +function wc_query_string_form_fields( $values = null, $exclude = array(), $current_key = '', $return = false ) { + if ( is_null( $values ) ) { + $values = $_GET; + } + $html = ''; + + foreach ( $values as $key => $value ) { + if ( in_array( $key, $exclude, true ) ) { + continue; + } + if ( $current_key ) { + $key = $current_key . '[' . $key . ']'; + } + if ( is_array( $value ) ) { + $html .= wc_query_string_form_fields( $value, $exclude, $key, true ); + } else { + $html .= ''; + } + } + + if ( $return ) { + return $html; + } else { + echo $html; + } +} + /** Template pages ********************************************************/ if ( ! function_exists( 'woocommerce_content' ) ) { @@ -269,12 +421,10 @@ if ( ! function_exists( 'woocommerce_content' ) ) { /** * Output WooCommerce content. * - * This function is only used in the optional 'woocommerce.php' template - * which people can add to their themes to add basic woocommerce support + * This function is only used in the optional 'woocommerce.php' template. + * which people can add to their themes to add basic woocommerce support. * without hooks or modifying core templates. * - * @access public - * @return void */ function woocommerce_content() { @@ -298,7 +448,7 @@ if ( ! function_exists( 'woocommerce_content' ) ) { - + @@ -312,11 +462,11 @@ if ( ! function_exists( 'woocommerce_content' ) ) { - + woocommerce_product_loop_start( false ), 'after' => woocommerce_product_loop_end( false ) ) ) ) : ?> - + ' . $notice . '

    ' ); + if ( empty( $notice ) ) { + $notice = __( 'This is a demo store for testing purposes — no orders shall be fulfilled.', 'woocommerce' ); + } + + echo apply_filters( 'woocommerce_demo_store', '

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

    ', $notice ); } } @@ -391,17 +536,17 @@ if ( ! function_exists( 'woocommerce_page_title' ) ) { /** * woocommerce_page_title function. * - * @param boolean $echo + * @param bool $echo * @return string */ function woocommerce_page_title( $echo = true ) { if ( is_search() ) { - $page_title = sprintf( __( 'Search Results: “%s”', 'woocommerce' ), get_search_query() ); + $page_title = sprintf( __( 'Search results: “%s”', 'woocommerce' ), get_search_query() ); - if ( get_query_var( 'paged' ) ) + if ( get_query_var( 'paged' ) ) { $page_title .= sprintf( __( ' – Page %s', 'woocommerce' ), get_query_var( 'paged' ) ); - + } } elseif ( is_tax() ) { $page_title = single_term_title( "", false ); @@ -415,37 +560,38 @@ if ( ! function_exists( 'woocommerce_page_title' ) ) { $page_title = apply_filters( 'woocommerce_page_title', $page_title ); - if ( $echo ) - echo $page_title; - else - return $page_title; + if ( $echo ) { + echo $page_title; + } else { + return $page_title; + } } } if ( ! function_exists( 'woocommerce_product_loop_start' ) ) { /** - * Output the start of a product loop. By default this is a UL + * Output the start of a product loop. By default this is a UL. * - * @access public * @param bool $echo * @return string */ function woocommerce_product_loop_start( $echo = true ) { ob_start(); + $GLOBALS['woocommerce_loop']['loop'] = 0; wc_get_template( 'loop/loop-start.php' ); - if ( $echo ) + if ( $echo ) { echo ob_get_clean(); - else + } else { return ob_get_clean(); + } } } if ( ! function_exists( 'woocommerce_product_loop_end' ) ) { /** - * Output the end of a product loop. By default this is a UL + * Output the end of a product loop. By default this is a UL. * - * @access public * @param bool $echo * @return string */ @@ -454,24 +600,80 @@ if ( ! function_exists( 'woocommerce_product_loop_end' ) ) { wc_get_template( 'loop/loop-end.php' ); - if ( $echo ) + if ( $echo ) { echo ob_get_clean(); - else + } else { return ob_get_clean(); + } } } +if ( ! function_exists( 'woocommerce_template_loop_product_title' ) ) { + + /** + * Show the product title in the product loop. By default this is an H2. + */ + function woocommerce_template_loop_product_title() { + echo '

    ' . get_the_title() . '

    '; + } +} +if ( ! function_exists( 'woocommerce_template_loop_category_title' ) ) { + + /** + * Show the subcategory title in the product loop. + * + * @param object $category + */ + function woocommerce_template_loop_category_title( $category ) { + ?> +

    + name; + + if ( $category->count > 0 ) { + echo apply_filters( 'woocommerce_subcategory_count_html', ' (' . $category->count . ')', $category ); + } + ?> +

    + '; +} +/** + * Insert the opening anchor tag for products in the loop. + */ +function woocommerce_template_loop_product_link_close() { + echo ''; +} + +/** + * Insert the opening anchor tag for categories in the loop. + * + * @param int|object|string $category + */ +function woocommerce_template_loop_category_link_open( $category ) { + echo ''; +} +/** + * Insert the closing anchor tag for categories in the loop. + */ +function woocommerce_template_loop_category_link_close() { + echo ''; +} if ( ! function_exists( 'woocommerce_taxonomy_archive_description' ) ) { /** - * Show an archive description on taxonomy archives + * Show an archive description on taxonomy archives. * - * @access public * @subpackage Archives - * @return void */ function woocommerce_taxonomy_archive_description() { - if ( is_tax( array( 'product_cat', 'product_tag' ) ) && get_query_var( 'paged' ) == 0 ) { - $description = wpautop( do_shortcode( term_description() ) ); + if ( is_product_taxonomy() && 0 === absint( get_query_var( 'paged' ) ) ) { + $description = wc_format_content( term_description() ); if ( $description ) { echo '
    ' . $description . '
    '; } @@ -481,17 +683,20 @@ if ( ! function_exists( 'woocommerce_taxonomy_archive_description' ) ) { if ( ! function_exists( 'woocommerce_product_archive_description' ) ) { /** - * Show a shop page description on product archives + * Show a shop page description on product archives. * - * @access public * @subpackage Archives - * @return void */ function woocommerce_product_archive_description() { - if ( is_post_type_archive( 'product' ) && get_query_var( 'paged' ) == 0 ) { + // Don't display the description on search results page + if ( is_search() ) { + return; + } + + if ( is_post_type_archive( 'product' ) && 0 === absint( get_query_var( 'paged' ) ) ) { $shop_page = get_post( wc_get_page_id( 'shop' ) ); if ( $shop_page ) { - $description = wpautop( do_shortcode( $shop_page->post_content ) ); + $description = wc_format_content( $shop_page->post_content ); if ( $description ) { echo '
    ' . $description . '
    '; } @@ -505,12 +710,28 @@ if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) { /** * Get the add to cart template for the loop. * - * @access public - * @subpackage Loop - * @return void + * @subpackage Loop + * + * @param array $args */ function woocommerce_template_loop_add_to_cart( $args = array() ) { - wc_get_template( 'loop/add-to-cart.php' , $args ); + global $product; + + if ( $product ) { + $defaults = array( + 'quantity' => 1, + 'class' => implode( ' ', array_filter( array( + 'button', + 'product_type_' . $product->get_type(), + $product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '', + $product->supports( 'ajax_add_to_cart' ) ? 'ajax_add_to_cart' : '', + ) ) ), + ); + + $args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product ); + + wc_get_template( 'loop/add-to-cart.php', $args ); + } } } if ( ! function_exists( 'woocommerce_template_loop_product_thumbnail' ) ) { @@ -518,9 +739,7 @@ if ( ! function_exists( 'woocommerce_template_loop_product_thumbnail' ) ) { /** * Get the product thumbnail for the loop. * - * @access public * @subpackage Loop - * @return void */ function woocommerce_template_loop_product_thumbnail() { echo woocommerce_get_product_thumbnail(); @@ -531,9 +750,7 @@ if ( ! function_exists( 'woocommerce_template_loop_price' ) ) { /** * Get the product price for the loop. * - * @access public * @subpackage Loop - * @return void */ function woocommerce_template_loop_price() { wc_get_template( 'loop/price.php' ); @@ -542,11 +759,9 @@ if ( ! function_exists( 'woocommerce_template_loop_price' ) ) { if ( ! function_exists( 'woocommerce_template_loop_rating' ) ) { /** - * Display the average rating in the loop + * Display the average rating in the loop. * - * @access public * @subpackage Loop - * @return void */ function woocommerce_template_loop_rating() { wc_get_template( 'loop/rating.php' ); @@ -557,64 +772,30 @@ if ( ! function_exists( 'woocommerce_show_product_loop_sale_flash' ) ) { /** * Get the sale flash for the loop. * - * @access public * @subpackage Loop - * @return void */ function woocommerce_show_product_loop_sale_flash() { wc_get_template( 'loop/sale-flash.php' ); } } -if ( ! function_exists( 'woocommerce_get_product_schema' ) ) { - - /** - * Get a products Schema - * @return string - */ - function woocommerce_get_product_schema() { - global $product; - - $schema = "Product"; - - // Downloadable product schema handling - if ( $product->is_downloadable() ) { - switch ( $product->download_type ) { - case 'application' : - $schema = "SoftwareApplication"; - break; - case 'music' : - $schema = "MusicAlbum"; - break; - default : - $schema = "Product"; - break; - } - } - - return 'http://schema.org/' . $schema; - } -} - if ( ! function_exists( 'woocommerce_get_product_thumbnail' ) ) { /** * Get the product thumbnail, or the placeholder if not set. * - * @access public * @subpackage Loop * @param string $size (default: 'shop_catalog') - * @param int $placeholder_width (default: 0) - * @param int $placeholder_height (default: 0) + * @param int $deprecated1 Deprecated since WooCommerce 2.0 (default: 0) + * @param int $deprecated2 Deprecated since WooCommerce 2.0 (default: 0) * @return string */ - function woocommerce_get_product_thumbnail( $size = 'shop_catalog', $placeholder_width = 0, $placeholder_height = 0 ) { - global $post; + function woocommerce_get_product_thumbnail( $size = 'shop_catalog', $deprecated1 = 0, $deprecated2 = 0 ) { + global $product; - if ( has_post_thumbnail() ) - return get_the_post_thumbnail( $post->ID, $size ); - elseif ( wc_placeholder_img_src() ) - return wc_placeholder_img( $size ); + $image_size = apply_filters( 'single_product_archive_thumbnail_size', $size ); + + return $product ? $product->get_image( $image_size ) : ''; } } @@ -623,9 +804,7 @@ if ( ! function_exists( 'woocommerce_result_count' ) ) { /** * Output the result count text (Showing x - x of x results). * - * @access public * @subpackage Loop - * @return void */ function woocommerce_result_count() { wc_get_template( 'loop/result-count.php' ); @@ -637,14 +816,35 @@ if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) { /** * Output the product sorting options. * - * @access public * @subpackage Loop - * @return void */ function woocommerce_catalog_ordering() { - $orderby = isset( $_GET['orderby'] ) ? wc_clean( $_GET['orderby'] ) : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby' ) ); + global $wp_query; - wc_get_template( 'loop/orderby.php', array( 'orderby' => $orderby ) ); + if ( 1 === (int) $wp_query->found_posts || ! woocommerce_products_will_display() || $wp_query->is_search() ) { + return; + } + + $orderby = isset( $_GET['orderby'] ) ? wc_clean( $_GET['orderby'] ) : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby' ) ); + $show_default_orderby = 'menu_order' === apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby' ) ); + $catalog_orderby_options = apply_filters( 'woocommerce_catalog_orderby', array( + 'menu_order' => __( 'Default sorting', 'woocommerce' ), + 'popularity' => __( 'Sort by popularity', 'woocommerce' ), + 'rating' => __( 'Sort by average rating', 'woocommerce' ), + 'date' => __( 'Sort by newness', 'woocommerce' ), + 'price' => __( 'Sort by price: low to high', 'woocommerce' ), + 'price-desc' => __( 'Sort by price: high to low', 'woocommerce' ), + ) ); + + if ( ! $show_default_orderby ) { + unset( $catalog_orderby_options['menu_order'] ); + } + + if ( 'no' === get_option( 'woocommerce_enable_review_rating' ) ) { + unset( $catalog_orderby_options['rating'] ); + } + + wc_get_template( 'loop/orderby.php', array( 'catalog_orderby_options' => $catalog_orderby_options, 'orderby' => $orderby, 'show_default_orderby' => $show_default_orderby ) ); } } @@ -653,9 +853,7 @@ if ( ! function_exists( 'woocommerce_pagination' ) ) { /** * Output the pagination. * - * @access public * @subpackage Loop - * @return void */ function woocommerce_pagination() { wc_get_template( 'loop/pagination.php' ); @@ -669,9 +867,7 @@ if ( ! function_exists( 'woocommerce_show_product_images' ) ) { /** * Output the product image before the single product summary. * - * @access public * @subpackage Product - * @return void */ function woocommerce_show_product_images() { wc_get_template( 'single-product/product-image.php' ); @@ -682,9 +878,7 @@ if ( ! function_exists( 'woocommerce_show_product_thumbnails' ) ) { /** * Output the product thumbnails. * - * @access public * @subpackage Product - * @return void */ function woocommerce_show_product_thumbnails() { wc_get_template( 'single-product/product-thumbnails.php' ); @@ -695,9 +889,7 @@ if ( ! function_exists( 'woocommerce_output_product_data_tabs' ) ) { /** * Output the product tabs. * - * @access public * @subpackage Product/Tabs - * @return void */ function woocommerce_output_product_data_tabs() { wc_get_template( 'single-product/tabs/tabs.php' ); @@ -708,9 +900,7 @@ if ( ! function_exists( 'woocommerce_template_single_title' ) ) { /** * Output the product title. * - * @access public * @subpackage Product - * @return void */ function woocommerce_template_single_title() { wc_get_template( 'single-product/title.php' ); @@ -721,12 +911,12 @@ if ( ! function_exists( 'woocommerce_template_single_rating' ) ) { /** * Output the product rating. * - * @access public * @subpackage Product - * @return void */ function woocommerce_template_single_rating() { - wc_get_template( 'single-product/rating.php' ); + if ( post_type_supports( 'product', 'comments' ) ) { + wc_get_template( 'single-product/rating.php' ); + } } } if ( ! function_exists( 'woocommerce_template_single_price' ) ) { @@ -734,9 +924,7 @@ if ( ! function_exists( 'woocommerce_template_single_price' ) ) { /** * Output the product price. * - * @access public * @subpackage Product - * @return void */ function woocommerce_template_single_price() { wc_get_template( 'single-product/price.php' ); @@ -747,9 +935,7 @@ if ( ! function_exists( 'woocommerce_template_single_excerpt' ) ) { /** * Output the product short description (excerpt). * - * @access public * @subpackage Product - * @return void */ function woocommerce_template_single_excerpt() { wc_get_template( 'single-product/short-description.php' ); @@ -760,9 +946,7 @@ if ( ! function_exists( 'woocommerce_template_single_meta' ) ) { /** * Output the product meta. * - * @access public * @subpackage Product - * @return void */ function woocommerce_template_single_meta() { wc_get_template( 'single-product/meta.php' ); @@ -773,9 +957,7 @@ if ( ! function_exists( 'woocommerce_template_single_sharing' ) ) { /** * Output the product sharing. * - * @access public * @subpackage Product - * @return void */ function woocommerce_template_single_sharing() { wc_get_template( 'single-product/share.php' ); @@ -786,9 +968,7 @@ if ( ! function_exists( 'woocommerce_show_product_sale_flash' ) ) { /** * Output the product sale flash. * - * @access public * @subpackage Product - * @return void */ function woocommerce_show_product_sale_flash() { wc_get_template( 'single-product/sale-flash.php' ); @@ -800,13 +980,11 @@ if ( ! function_exists( 'woocommerce_template_single_add_to_cart' ) ) { /** * Trigger the single product add to cart action. * - * @access public * @subpackage Product - * @return void */ function woocommerce_template_single_add_to_cart() { global $product; - do_action( 'woocommerce_' . $product->product_type . '_add_to_cart' ); + do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' ); } } if ( ! function_exists( 'woocommerce_simple_add_to_cart' ) ) { @@ -814,9 +992,7 @@ if ( ! function_exists( 'woocommerce_simple_add_to_cart' ) ) { /** * Output the simple product add to cart area. * - * @access public * @subpackage Product - * @return void */ function woocommerce_simple_add_to_cart() { wc_get_template( 'single-product/add-to-cart/simple.php' ); @@ -827,18 +1003,22 @@ if ( ! function_exists( 'woocommerce_grouped_add_to_cart' ) ) { /** * Output the grouped product add to cart area. * - * @access public * @subpackage Product - * @return void */ function woocommerce_grouped_add_to_cart() { global $product; - wc_get_template( 'single-product/add-to-cart/grouped.php', array( - 'grouped_product' => $product, - 'grouped_products' => $product->get_children(), - 'quantites_required' => false - ) ); + $products = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' ); + + if ( $products ) { + usort( $products, 'wc_products_array_orderby_menu_order' ); + + wc_get_template( 'single-product/add-to-cart/grouped.php', array( + 'grouped_product' => $product, + 'grouped_products' => $products, + 'quantites_required' => false, + ) ); + } } } if ( ! function_exists( 'woocommerce_variable_add_to_cart' ) ) { @@ -846,9 +1026,7 @@ if ( ! function_exists( 'woocommerce_variable_add_to_cart' ) ) { /** * Output the variable product add to cart area. * - * @access public * @subpackage Product - * @return void */ function woocommerce_variable_add_to_cart() { global $product; @@ -856,12 +1034,15 @@ if ( ! function_exists( 'woocommerce_variable_add_to_cart' ) ) { // Enqueue variation scripts wp_enqueue_script( 'wc-add-to-cart-variation' ); + // Get Available variations? + $get_variations = sizeof( $product->get_children() ) <= apply_filters( 'woocommerce_ajax_variation_threshold', 30, $product ); + // Load the template wc_get_template( 'single-product/add-to-cart/variable.php', array( - 'available_variations' => $product->get_available_variations(), - 'attributes' => $product->get_variation_attributes(), - 'selected_attributes' => $product->get_variation_default_attributes() - ) ); + 'available_variations' => $get_variations ? $product->get_available_variations() : false, + 'attributes' => $product->get_variation_attributes(), + 'selected_attributes' => $product->get_default_attributes(), + ) ); } } if ( ! function_exists( 'woocommerce_external_add_to_cart' ) ) { @@ -869,20 +1050,19 @@ if ( ! function_exists( 'woocommerce_external_add_to_cart' ) ) { /** * Output the external product add to cart area. * - * @access public * @subpackage Product - * @return void */ function woocommerce_external_add_to_cart() { global $product; - if ( ! $product->get_product_url() ) + if ( ! $product->add_to_cart_url() ) { return; + } wc_get_template( 'single-product/add-to-cart/external.php', array( - 'product_url' => $product->get_product_url(), - 'button_text' => $product->single_add_to_cart_text() - ) ); + 'product_url' => $product->add_to_cart_url(), + 'button_text' => $product->single_add_to_cart_text(), + ) ); } } @@ -893,23 +1073,36 @@ if ( ! function_exists( 'woocommerce_quantity_input' ) ) { * * @param array $args Args for the input * @param WC_Product|null $product - * @param boolean $echo Whether to return or echo - * @return void|string + * @param boolean $echo Whether to return or echo|string + * + * @return string */ function woocommerce_quantity_input( $args = array(), $product = null, $echo = true ) { - if ( is_null( $product ) ) + if ( is_null( $product ) ) { $product = $GLOBALS['product']; + } $defaults = array( - 'input_name' => 'quantity', - 'input_value' => '1', - 'max_value' => apply_filters( 'woocommerce_quantity_input_max', '', $product ), - 'min_value' => apply_filters( 'woocommerce_quantity_input_min', '', $product ), - 'step' => apply_filters( 'woocommerce_quantity_input_step', '1', $product ) + 'input_name' => 'quantity', + 'input_value' => '1', + 'max_value' => apply_filters( 'woocommerce_quantity_input_max', -1, $product ), + 'min_value' => apply_filters( 'woocommerce_quantity_input_min', 0, $product ), + 'step' => apply_filters( 'woocommerce_quantity_input_step', 1, $product ), + 'pattern' => apply_filters( 'woocommerce_quantity_input_pattern', has_filter( 'woocommerce_stock_amount', 'intval' ) ? '[0-9]*' : '' ), + 'inputmode' => apply_filters( 'woocommerce_quantity_input_inputmode', has_filter( 'woocommerce_stock_amount', 'intval' ) ? 'numeric' : '' ), ); $args = apply_filters( 'woocommerce_quantity_input_args', wp_parse_args( $args, $defaults ), $product ); + // Apply sanity to min/max args - min cannot be lower than 0. + $args['min_value'] = max( $args['min_value'], 0 ); + $args['max_value'] = 0 < $args['max_value'] ? $args['max_value'] : ''; + + // Max cannot be lower than min if defined. + if ( '' !== $args['max_value'] && $args['max_value'] < $args['min_value'] ) { + $args['max_value'] = $args['min_value']; + } + ob_start(); wc_get_template( 'global/quantity-input.php', $args ); @@ -927,9 +1120,7 @@ if ( ! function_exists( 'woocommerce_product_description_tab' ) ) { /** * Output the description tab content. * - * @access public * @subpackage Product/Tabs - * @return void */ function woocommerce_product_description_tab() { wc_get_template( 'single-product/tabs/description.php' ); @@ -940,9 +1131,7 @@ if ( ! function_exists( 'woocommerce_product_additional_information_tab' ) ) { /** * Output the attributes tab content. * - * @access public * @subpackage Product/Tabs - * @return void */ function woocommerce_product_additional_information_tab() { wc_get_template( 'single-product/tabs/additional-information.php' ); @@ -952,13 +1141,11 @@ if ( ! function_exists( 'woocommerce_product_reviews_tab' ) ) { /** * Output the reviews tab content. - * - * @access public + * @deprecated 2.4.0 Unused * @subpackage Product/Tabs - * @return void */ function woocommerce_product_reviews_tab() { - wc_get_template( 'single-product/tabs/reviews.php' ); + wc_deprecated_function( 'woocommerce_product_reviews_tab', '2.4' ); } } @@ -967,7 +1154,6 @@ if ( ! function_exists( 'woocommerce_default_product_tabs' ) ) { /** * Add default product tabs to product pages. * - * @access public * @param array $tabs * @return array */ @@ -979,25 +1165,25 @@ if ( ! function_exists( 'woocommerce_default_product_tabs' ) ) { $tabs['description'] = array( 'title' => __( 'Description', 'woocommerce' ), 'priority' => 10, - 'callback' => 'woocommerce_product_description_tab' + 'callback' => 'woocommerce_product_description_tab', ); } // Additional information tab - shows attributes - if ( $product && ( $product->has_attributes() || ( $product->enable_dimensions_display() && ( $product->has_dimensions() || $product->has_weight() ) ) ) ) { + if ( $product && ( $product->has_attributes() || apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() ) ) ) { $tabs['additional_information'] = array( - 'title' => __( 'Additional Information', 'woocommerce' ), + 'title' => __( 'Additional information', 'woocommerce' ), 'priority' => 20, - 'callback' => 'woocommerce_product_additional_information_tab' + 'callback' => 'woocommerce_product_additional_information_tab', ); } // Reviews tab - shows comments if ( comments_open() ) { $tabs['reviews'] = array( - 'title' => sprintf( __( 'Reviews (%d)', 'woocommerce' ), get_comments_number( $post->ID ) ), + 'title' => sprintf( __( 'Reviews (%d)', 'woocommerce' ), $product->get_review_count() ), 'priority' => 30, - 'callback' => 'comments_template' + 'callback' => 'comments_template', ); } @@ -1008,9 +1194,8 @@ if ( ! function_exists( 'woocommerce_default_product_tabs' ) ) { if ( ! function_exists( 'woocommerce_sort_product_tabs' ) ) { /** - * Sort tabs by priority + * Sort tabs by priority. * - * @access public * @param array $tabs * @return array */ @@ -1019,15 +1204,16 @@ if ( ! function_exists( 'woocommerce_sort_product_tabs' ) ) { // Make sure the $tabs parameter is an array if ( ! is_array( $tabs ) ) { trigger_error( "Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array." ); - $tabs = array( ); + $tabs = array(); } // Re-order tabs by priority if ( ! function_exists( '_sort_priority_callback' ) ) { function _sort_priority_callback( $a, $b ) { - if ( $a['priority'] == $b['priority'] ) - return 0; - return ( $a['priority'] < $b['priority'] ) ? -1 : 1; + if ( $a['priority'] === $b['priority'] ) { + return 0; + } + return ( $a['priority'] < $b['priority'] ) ? -1 : 1; } } @@ -1042,9 +1228,10 @@ if ( ! function_exists( 'woocommerce_comments' ) ) { /** * Output the Review comments template. * - * @access public * @subpackage Product - * @return void + * @param WP_Comment $comment + * @param array $args + * @param int $depth */ function woocommerce_comments( $comment, $args, $depth ) { $GLOBALS['comment'] = $comment; @@ -1052,21 +1239,67 @@ if ( ! function_exists( 'woocommerce_comments' ) ) { } } +if ( ! function_exists( 'woocommerce_review_display_gravatar' ) ) { + /** + * Display the review authors gravatar + * + * @param array $comment WP_Comment. + * @return void + */ + function woocommerce_review_display_gravatar( $comment ) { + echo get_avatar( $comment, apply_filters( 'woocommerce_review_gravatar_size', '60' ), '' ); + } +} + +if ( ! function_exists( 'woocommerce_review_display_rating' ) ) { + /** + * Display the reviewers star rating + * + * @return void + */ + function woocommerce_review_display_rating() { + if ( post_type_supports( 'product', 'comments' ) ) { + wc_get_template( 'single-product/review-rating.php' ); + } + } +} + +if ( ! function_exists( 'woocommerce_review_display_meta' ) ) { + /** + * Display the review authors meta (name, verified owner, review date) + * + * @return void + */ + function woocommerce_review_display_meta() { + wc_get_template( 'single-product/review-meta.php' ); + } +} + +if ( ! function_exists( 'woocommerce_review_display_comment_text' ) ) { + + /** + * Display the review content. + */ + function woocommerce_review_display_comment_text() { + echo '
    '; + comment_text(); + echo '
    '; + } +} + if ( ! function_exists( 'woocommerce_output_related_products' ) ) { /** * Output the related products. * - * @access public * @subpackage Product - * @return void */ function woocommerce_output_related_products() { $args = array( - 'posts_per_page' => 2, - 'columns' => 2, - 'orderby' => 'rand' + 'posts_per_page' => 4, + 'columns' => 4, + 'orderby' => 'rand', ); woocommerce_related_products( apply_filters( 'woocommerce_output_related_products_args', $args ) ); @@ -1078,33 +1311,34 @@ if ( ! function_exists( 'woocommerce_related_products' ) ) { /** * Output the related products. * - * @access public * @param array Provided arguments - * @param bool Columns argument for backwards compat - * @param bool Order by argument for backwards compat - * @return void */ - function woocommerce_related_products( $args = array(), $columns = false, $orderby = false ) { - if ( ! is_array( $args ) ) { - _deprecated_argument( __FUNCTION__, '2.1', __( 'Use $args argument as an array instead. Deprecated argument will be removed in WC 2.2.', 'woocommerce' ) ); + function woocommerce_related_products( $args = array() ) { + global $product, $woocommerce_loop; - $argsvalue = $args; - - $args = array( - 'posts_per_page' => $argsvalue, - 'columns' => $columns, - 'orderby' => $orderby, - ); + if ( ! $product ) { + return; } $defaults = array( 'posts_per_page' => 2, 'columns' => 2, - 'orderby' => 'rand' + 'orderby' => 'rand', + 'order' => 'desc', ); $args = wp_parse_args( $args, $defaults ); + // Get visble related products then sort them at random. + $args['related_products'] = array_filter( array_map( 'wc_get_product', wc_get_related_products( $product->get_id(), $args['posts_per_page'], $product->get_upsell_ids() ) ), 'wc_products_array_filter_visible' ); + + // Handle orderby. + $args['related_products'] = wc_products_array_orderby( $args['related_products'], $args['orderby'], $args['order'] ); + + // Set global loop values. + $woocommerce_loop['name'] = 'related'; + $woocommerce_loop['columns'] = apply_filters( 'woocommerce_related_products_columns', $args['columns'] ); + wc_get_template( 'single-product/related.php', $args ); } } @@ -1114,18 +1348,41 @@ if ( ! function_exists( 'woocommerce_upsell_display' ) ) { /** * Output product up sells. * - * @access public - * @param int $posts_per_page (default: -1) - * @param int $columns (default: 2) - * @param string $orderby (default: 'rand') - * @return void + * @param int $limit (default: -1) + * @param int $columns (default: 4) + * @param string $orderby Supported values - rand, title, ID, date, modified, menu_order, price. + * @param string $order Sort direction. */ - function woocommerce_upsell_display( $posts_per_page = '-1', $columns = 2, $orderby = 'rand' ) { + function woocommerce_upsell_display( $limit = '-1', $columns = 4, $orderby = 'rand', $order = 'desc' ) { + global $product, $woocommerce_loop; + + if ( ! $product ) { + return; + } + + // Handle the legacy filter which controlled posts per page etc. + $args = apply_filters( 'woocommerce_upsell_display_args', array( + 'posts_per_page' => $limit, + 'orderby' => $orderby, + 'columns' => $columns, + ) ); + $woocommerce_loop['name'] = 'up-sells'; + $woocommerce_loop['columns'] = apply_filters( 'woocommerce_upsells_columns', isset( $args['columns'] ) ? $args['columns'] : $columns ); + $orderby = apply_filters( 'woocommerce_upsells_orderby', isset( $args['orderby'] ) ? $args['orderby'] : $orderby ); + $limit = apply_filters( 'woocommerce_upsells_total', isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : $limit ); + + // Get visble upsells then sort them at random, then limit result set. + $upsells = wc_products_array_orderby( array_filter( array_map( 'wc_get_product', $product->get_upsell_ids() ), 'wc_products_array_filter_visible' ), $orderby, $order ); + $upsells = $limit > 0 ? array_slice( $upsells, 0, $limit ) : $upsells; + wc_get_template( 'single-product/up-sells.php', array( - 'posts_per_page' => $posts_per_page, - 'orderby' => apply_filters( 'woocommerce_upsells_orderby', $orderby ), - 'columns' => $columns - ) ); + 'upsells' => $upsells, + + // Not used now, but used in previous version of up-sells.php. + 'posts_per_page' => $limit, + 'orderby' => $orderby, + 'columns' => $columns, + ) ); } } @@ -1136,9 +1393,7 @@ if ( ! function_exists( 'woocommerce_shipping_calculator' ) ) { /** * Output the cart shipping calculator. * - * @access public * @subpackage Cart - * @return void */ function woocommerce_shipping_calculator() { wc_get_template( 'cart/shipping-calculator.php' ); @@ -1150,11 +1405,12 @@ if ( ! function_exists( 'woocommerce_cart_totals' ) ) { /** * Output the cart totals. * - * @access public * @subpackage Cart - * @return void */ function woocommerce_cart_totals() { + if ( is_checkout() ) { + return; + } wc_get_template( 'cart/cart-totals.php' ); } } @@ -1164,16 +1420,72 @@ if ( ! function_exists( 'woocommerce_cross_sell_display' ) ) { /** * Output the cart cross-sells. * - * @param integer $posts_per_page - * @param integer $columns - * @param string $orderby + * @param int $limit (default: 2) + * @param int $columns (default: 2) + * @param string $orderby (default: 'rand') + * @param string $order (default: 'desc') */ - function woocommerce_cross_sell_display( $posts_per_page = 2, $columns = 2, $orderby = 'rand' ) { + function woocommerce_cross_sell_display( $limit = 2, $columns = 2, $orderby = 'rand', $order = 'desc' ) { + global $woocommerce_loop; + + if ( is_checkout() ) { + return; + } + // Get visble cross sells then sort them at random. + $cross_sells = array_filter( array_map( 'wc_get_product', WC()->cart->get_cross_sells() ), 'wc_products_array_filter_visible' ); + $woocommerce_loop['name'] = 'cross-sells'; + $woocommerce_loop['columns'] = apply_filters( 'woocommerce_cross_sells_columns', $columns ); + + // Handle orderby and limit results. + $orderby = apply_filters( 'woocommerce_cross_sells_orderby', $orderby ); + $cross_sells = wc_products_array_orderby( $cross_sells, $orderby, $order ); + $limit = apply_filters( 'woocommerce_cross_sells_total', $limit ); + $cross_sells = $limit > 0 ? array_slice( $cross_sells, 0, $limit ) : $cross_sells; + wc_get_template( 'cart/cross-sells.php', array( - 'posts_per_page' => $posts_per_page, - 'orderby' => $orderby, - 'columns' => $columns - ) ); + 'cross_sells' => $cross_sells, + + // Not used now, but used in previous version of up-sells.php. + 'posts_per_page' => $limit, + 'orderby' => $orderby, + 'columns' => $columns, + ) ); + } +} + +if ( ! function_exists( 'woocommerce_button_proceed_to_checkout' ) ) { + + /** + * Output the proceed to checkout button. + * + * @subpackage Cart + */ + function woocommerce_button_proceed_to_checkout() { + wc_get_template( 'cart/proceed-to-checkout-button.php' ); + } +} + +if ( ! function_exists( 'woocommerce_widget_shopping_cart_button_view_cart' ) ) { + + /** + * Output the proceed to checkout button. + * + * @subpackage Cart + */ + function woocommerce_widget_shopping_cart_button_view_cart() { + echo '' . esc_html__( 'View cart', 'woocommerce' ) . ''; + } +} + +if ( ! function_exists( 'woocommerce_widget_shopping_cart_proceed_to_checkout' ) ) { + + /** + * Output the proceed to checkout button. + * + * @subpackage Cart + */ + function woocommerce_widget_shopping_cart_proceed_to_checkout() { + echo '' . esc_html__( 'Checkout', 'woocommerce' ) . ''; } } @@ -1182,15 +1494,14 @@ if ( ! function_exists( 'woocommerce_cross_sell_display' ) ) { if ( ! function_exists( 'woocommerce_mini_cart' ) ) { /** - * Output the Mini-cart - used by cart widget + * Output the Mini-cart - used by cart widget. * - * @access public - * @return void + * @param array $args */ function woocommerce_mini_cart( $args = array() ) { $defaults = array( - 'list_class' => '' + 'list_class' => '', ); $args = wp_parse_args( $args, $defaults ); @@ -1204,21 +1515,20 @@ if ( ! function_exists( 'woocommerce_mini_cart' ) ) { if ( ! function_exists( 'woocommerce_login_form' ) ) { /** - * Output the WooCommerce Login Form + * Output the WooCommerce Login Form. * - * @access public * @subpackage Forms - * @return void + * @param array $args */ function woocommerce_login_form( $args = array() ) { $defaults = array( 'message' => '', 'redirect' => '', - 'hidden' => false + 'hidden' => false, ); - $args = wp_parse_args( $args, $defaults ); + $args = wp_parse_args( $args, $defaults ); wc_get_template( 'global/form-login.php', $args ); } @@ -1227,11 +1537,9 @@ if ( ! function_exists( 'woocommerce_login_form' ) ) { if ( ! function_exists( 'woocommerce_checkout_login_form' ) ) { /** - * Output the WooCommerce Checkout Login Form + * Output the WooCommerce Checkout Login Form. * - * @access public * @subpackage Checkout - * @return void */ function woocommerce_checkout_login_form() { wc_get_template( 'checkout/form-login.php', array( 'checkout' => WC()->checkout() ) ); @@ -1241,23 +1549,32 @@ if ( ! function_exists( 'woocommerce_checkout_login_form' ) ) { if ( ! function_exists( 'woocommerce_breadcrumb' ) ) { /** - * Output the WooCommerce Breadcrumb + * Output the WooCommerce Breadcrumb. * - * @access public - * @return void + * @param array $args */ function woocommerce_breadcrumb( $args = array() ) { - - $defaults = apply_filters( 'woocommerce_breadcrumb_defaults', array( - 'delimiter' => ' / ', - 'wrap_before' => '